diff --git a/AUTHORS b/AUTHORS index de363158b7..34d60b0881 100644 --- a/AUTHORS +++ b/AUTHORS @@ -20,6 +20,7 @@ Also thanks to: * Akseli Virtanen (RAGEQUIT) * Andrew Perkins * Andrew Riedi + * Andrew Aldridge (i80and) * Andreas Beck (baxtor) * Barnaby Smith (mvi) * Bellator diff --git a/OpenRA.Game/CVec.cs b/OpenRA.Game/CVec.cs index d21a4c21ff..aad8d732d3 100644 --- a/OpenRA.Game/CVec.cs +++ b/OpenRA.Game/CVec.cs @@ -73,5 +73,17 @@ namespace OpenRA } public override string ToString() { return "{0},{1}".F(X, Y); } + + public static readonly CVec[] directions = + { + new CVec(-1, -1), + new CVec(-1, 0), + new CVec(-1, 1), + new CVec(0, -1), + new CVec(0, 1), + new CVec(1, -1), + new CVec(1, 0), + new CVec(1, 1), + }; } } diff --git a/OpenRA.Mods.RA/Bridge.cs b/OpenRA.Mods.RA/Bridge.cs index b889b4e8c9..ea51b80d62 100644 --- a/OpenRA.Mods.RA/Bridge.cs +++ b/OpenRA.Mods.RA/Bridge.cs @@ -218,6 +218,12 @@ namespace OpenRA.Mods.RA foreach (var c in TileSprites[currentTemplate].Keys) self.World.Map.CustomTerrain[c.X, c.Y] = GetTerrainType(c); + // If this bridge repair operation connects two pathfinding domains, + // update the domain index. + var domainIndex = self.World.WorldActor.TraitOrDefault(); + if (domainIndex != null) + domainIndex.UpdateCells(self.World, TileSprites[currentTemplate].Keys); + if (LongBridgeSegmentIsDead() && !killedUnits) { killedUnits = true; diff --git a/OpenRA.Mods.RA/Move/PathFinder.cs b/OpenRA.Mods.RA/Move/PathFinder.cs index 5345bb0702..037f312cfb 100755 --- a/OpenRA.Mods.RA/Move/PathFinder.cs +++ b/OpenRA.Mods.RA/Move/PathFinder.cs @@ -25,6 +25,8 @@ namespace OpenRA.Mods.RA.Move public class PathFinder { + readonly static List emptyPath = new List(0); + readonly World world; public PathFinder(World world) { this.world = world; } @@ -50,11 +52,20 @@ namespace OpenRA.Mods.RA.Move Log.Write("debug", "Actor {0} asked for a path from {1} tick(s) ago", self.ActorID, world.FrameNumber - cached.tick); if (world.FrameNumber - cached.tick > MaxPathAge) CachedPaths.Remove(cached); - return new List(cached.result); + return emptyPath; } 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 pb = FindBidiPath( PathSearch.FromPoint(world, mi, self, target, from, true), PathSearch.FromPoint(world, mi, self, from, target, true).InReverse() @@ -86,6 +97,17 @@ namespace OpenRA.Mods.RA.Move .Where(t => (t.CenterPosition - target).LengthSquared <= rangeSquared && mi.CanEnterCell(self.World, self, t, null, true, true)); + // 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) + { + var passable = mi.GetMovementClass(world.TileSet); + tilesInRange = new List(tilesInRange.Where(t => domainIndex.IsPassable(src, t, (uint)passable))); + if (tilesInRange.Count() == 0) + return emptyPath; + } + var path = FindBidiPath( PathSearch.FromPoints(world, mi, self, tilesInRange, src, true), PathSearch.FromPoint(world, mi, self, src, targetCell, true).InReverse() @@ -124,7 +146,7 @@ namespace OpenRA.Mods.RA.Move } // no path exists - return new List(0); + return emptyPath; } } @@ -189,7 +211,7 @@ namespace OpenRA.Mods.RA.Move return path; } - return new List(0); + return emptyPath; } } diff --git a/OpenRA.Mods.RA/Move/PathSearch.cs b/OpenRA.Mods.RA/Move/PathSearch.cs index 2775e3a3de..efd79d2c7e 100755 --- a/OpenRA.Mods.RA/Move/PathSearch.cs +++ b/OpenRA.Mods.RA/Move/PathSearch.cs @@ -44,7 +44,7 @@ namespace OpenRA.Mods.RA.Move queue = new PriorityQueue(); considered = new HashSet(); maxCost = 0; - nextDirections = directions.Select(d => new Pair(d, 0)).ToArray(); + nextDirections = CVec.directions.Select(d => new Pair(d, 0)).ToArray(); } public PathSearch InReverse() @@ -190,18 +190,6 @@ namespace OpenRA.Mods.RA.Move return p.Location; } - static readonly CVec[] directions = - { - new CVec( -1, -1 ), - new CVec( -1, 0 ), - new CVec( -1, 1 ), - new CVec( 0, -1 ), - new CVec( 0, 1 ), - new CVec( 1, -1 ), - new CVec( 1, 0 ), - new CVec( 1, 1 ), - }; - public void AddInitialCell(CPos location) { if (!world.Map.IsInMap(location.X, location.Y)) diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj index 6713cdc609..249cf6de26 100644 --- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj +++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj @@ -457,6 +457,7 @@ + diff --git a/OpenRA.Mods.RA/World/DomainIndex.cs b/OpenRA.Mods.RA/World/DomainIndex.cs new file mode 100644 index 0000000000..d23e11fdae --- /dev/null +++ b/OpenRA.Mods.RA/World/DomainIndex.cs @@ -0,0 +1,249 @@ +#region Copyright & License Information +/* + * Copyright 2007-2013 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.FileFormats; +using OpenRA.Mods.RA.Move; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.RA +{ + // Identify untraversable regions of the map for faster pathfinding, especially with AI + class DomainIndexInfo : TraitInfo {} + + public class DomainIndex : IWorldLoaded + { + Dictionary domainIndexes; + + public void WorldLoaded(World world) + { + domainIndexes = new Dictionary(); + var movementClasses = new HashSet( + Rules.Info.Where(ai => ai.Value.Traits.Contains()) + .Select(ai => (uint)ai.Value.Traits.Get().GetMovementClass(world.TileSet))); + + foreach (var mc in movementClasses) + domainIndexes[mc] = new MovementClassDomainIndex(world, mc); + } + + public bool IsPassable(CPos p1, CPos p2, uint movementClass) + { + return domainIndexes[movementClass].IsPassable(p1, p2); + } + + /// Regenerate the domain index for a group of cells + public void UpdateCells(World world, IEnumerable cells) + { + var dirty = new HashSet(cells); + foreach (var index in domainIndexes) + index.Value.UpdateCells(world, dirty); + } + } + + class MovementClassDomainIndex + { + Rectangle bounds; + + uint movementClass; + int[,] domains; + Dictionary> transientConnections; + + // Each terrain has an offset corresponding to its location in a + // movement class bitmask. This caches each offset. + Dictionary terrainOffsets; + + public MovementClassDomainIndex(World world, uint movementClass) + { + bounds = world.Map.Bounds; + this.movementClass = movementClass; + domains = new int[(bounds.Width + bounds.X), (bounds.Height + bounds.Y)]; + transientConnections = new Dictionary>(); + + terrainOffsets = new Dictionary(); + var terrains = world.TileSet.Terrain.OrderBy(t => t.Key).ToList(); + foreach (var terrain in terrains) + { + var terrainOffset = terrains.FindIndex(x => x.Key == terrain.Key); + terrainOffsets[terrain.Key] = terrainOffset; + } + + BuildDomains(world); + } + + public bool IsPassable(CPos p1, CPos p2) + { + if (domains[p1.X, p1.Y] == domains[p2.X, p2.Y]) + return true; + + // Even though p1 and p2 are in different domains, it's possible + // that some dynamic terrain (i.e. bridges) may connect them. + return HasConnection(GetDomainOf(p1), GetDomainOf(p2)); + } + + public void UpdateCells(World world, HashSet dirtyCells) + { + var neighborDomains = new List(); + + foreach (var cell in dirtyCells) + { + // Select all neighbors inside the map boundries + var neighbors = CVec.directions.Select(d => d + cell) + .Where(c => bounds.Contains(c.X, c.Y)); + + var found = false; + foreach (var neighbor in neighbors) + { + if (!dirtyCells.Contains(neighbor)) + { + var neighborDomain = GetDomainOf(neighbor); + + var match = CanTraverseTile(world, neighbor); + if (match) neighborDomains.Add(neighborDomain); + + // Set ourselves to the first non-dirty neighbor we find. + if (!found) + { + SetDomain(cell, neighborDomain); + found = true; + } + } + } + } + + foreach (var c1 in neighborDomains) + { + foreach (var c2 in neighborDomains) + CreateConnection(c1, c2); + } + } + + int GetDomainOf(CPos p) + { + return domains[p.X, p.Y]; + } + + void SetDomain(CPos p, int domain) + { + domains[p.X, p.Y] = domain; + } + + bool HasConnection(int d1, int d2) + { + // Search our connections graph for a possible route + var visited = new HashSet(); + var toProcess = new Stack(); + toProcess.Push(d1); + + var i = 0; + while (toProcess.Count() > 0) + { + var current = toProcess.Pop(); + if (!transientConnections.ContainsKey(current)) + continue; + + foreach (int neighbor in transientConnections[current]) + { + if (neighbor == d2) + return true; + if (!visited.Contains(neighbor)) + toProcess.Push(neighbor); + } + + visited.Add(current); + i += 1; + } + + return false; + } + + void CreateConnection(int d1, int d2) + { + if (!transientConnections.ContainsKey(d1)) + transientConnections[d1] = new HashSet(); + if (!transientConnections.ContainsKey(d2)) + transientConnections[d2] = new HashSet(); + + transientConnections[d1].Add(d2); + transientConnections[d2].Add(d1); + } + + bool CanTraverseTile(World world, CPos p) + { + var currentTileType = WorldUtils.GetTerrainType(world, p); + var terrainOffset = terrainOffsets[currentTileType]; + return (movementClass & (1 << terrainOffset)) > 0; + } + + void BuildDomains(World world) + { + var timer = new Stopwatch(); + var map = world.Map; + + var domain = 1; + + var visited = new bool[(bounds.Width + bounds.X), (bounds.Height + bounds.Y)]; + + var toProcess = new Queue(); + toProcess.Enqueue(new CPos(map.Bounds.Left, map.Bounds.Top)); + + // Flood-fill over each domain + while (toProcess.Count != 0) + { + var start = toProcess.Dequeue(); + + // Technically redundant with the check in the inner loop, but prevents + // ballooning the domain counter. + if (visited[start.X, start.Y]) + continue; + + var domainQueue = new Queue(); + domainQueue.Enqueue(start); + + var currentPassable = CanTraverseTile(world, start); + + // Add all contiguous cells to our domain, and make a note of + // any non-contiguous cells for future domains + while (domainQueue.Count != 0) + { + var n = domainQueue.Dequeue(); + if (visited[n.X, n.Y]) + continue; + + var candidatePassable = CanTraverseTile(world, n); + if (candidatePassable != currentPassable) + { + toProcess.Enqueue(n); + continue; + } + + visited[n.X, n.Y] = true; + SetDomain(n, domain); + + // Don't crawl off the map, or add already-visited cells + var neighbors = CVec.directions.Select(d => n + d) + .Where(p => (p.X < map.Bounds.Right) && (p.X >= map.Bounds.Left) + && (p.Y >= map.Bounds.Top) && (p.Y < map.Bounds.Bottom) + && !visited[p.X, p.Y]); + foreach (var neighbor in neighbors) + domainQueue.Enqueue(neighbor); + } + + domain += 1; + } + + Log.Write("debug", "{0}: Found {1} domains. Took {2} s", map.Title, domain-1, timer.ElapsedTime()); + } + } +} diff --git a/mods/cnc/rules/system.yaml b/mods/cnc/rules/system.yaml index 1445012741..1f2920bce5 100644 --- a/mods/cnc/rules/system.yaml +++ b/mods/cnc/rules/system.yaml @@ -158,7 +158,7 @@ Player: silo: 1 BuildingFractions: proc: 17% - nuke: 10% + nuke: 10% pyle: 7% hand: 9% hq: 1% @@ -273,6 +273,7 @@ World: ProductionQueueFromSelection: ProductionTabsWidget: PRODUCTION_TABS BibLayer: + DomainIndex: ResourceLayer: ResourceClaimLayer: ResourceType@green-tib: diff --git a/mods/d2k/rules/system.yaml b/mods/d2k/rules/system.yaml index b6bbe67e14..79b5c49606 100644 --- a/mods/d2k/rules/system.yaml +++ b/mods/d2k/rules/system.yaml @@ -354,6 +354,7 @@ World: BibLayer: BibTypes: bib3x, bib2x BibWidths: 3, 2 + DomainIndex: ResourceLayer: ResourceClaimLayer: ResourceType@Spice: diff --git a/mods/ra/rules/system.yaml b/mods/ra/rules/system.yaml index 05ac3233c5..4953c644ec 100644 --- a/mods/ra/rules/system.yaml +++ b/mods/ra/rules/system.yaml @@ -604,6 +604,7 @@ World: Name: Soviet Race: soviet BibLayer: + DomainIndex: ResourceLayer: ResourceClaimLayer: ResourceType@ore: