Files
OpenRA/OpenRA.Mods.Common/Traits/World/DomainIndex.cs
Codes4Fun c9ed97524f Fix for 7539.
When the bridge damage state changes, the domain cells of the bridge
would pull in the first neighbors domain which may be untraversable.
This change only pulls the domain of traversable neighbor tiles.
2016-02-09 14:38:16 -08:00

225 lines
5.9 KiB
C#

#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.Graphics;
using OpenRA.Support;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[Desc("Identify untraversable regions of the map for faster pathfinding, especially with AI.",
"This trait is required. Every mod needs it attached to the world actor.")]
class DomainIndexInfo : TraitInfo<DomainIndex> { }
public class DomainIndex : IWorldLoaded
{
Dictionary<uint, MovementClassDomainIndex> domainIndexes;
public void WorldLoaded(World world, WorldRenderer wr)
{
domainIndexes = new Dictionary<uint, MovementClassDomainIndex>();
var movementClasses =
world.Map.Rules.Actors.Where(ai => ai.Value.HasTraitInfo<MobileInfo>())
.Select(ai => (uint)ai.Value.TraitInfo<MobileInfo>().GetMovementClass(world.TileSet)).Distinct();
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<CPos> cells)
{
var dirty = cells.ToHashSet();
foreach (var index in domainIndexes)
index.Value.UpdateCells(world, dirty);
}
}
class MovementClassDomainIndex
{
readonly Map map;
readonly uint movementClass;
readonly CellLayer<int> domains;
readonly Dictionary<int, HashSet<int>> transientConnections;
public MovementClassDomainIndex(World world, uint movementClass)
{
map = world.Map;
this.movementClass = movementClass;
domains = new CellLayer<int>(world.Map);
transientConnections = new Dictionary<int, HashSet<int>>();
using (new PerfTimer("BuildDomains: {0} for movement class {1}".F(world.Map.Title, movementClass)))
BuildDomains(world);
}
public bool IsPassable(CPos p1, CPos p2)
{
if (!domains.Contains(p1) || !domains.Contains(p2))
return false;
if (domains[p1] == domains[p2])
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(domains[p1], domains[p2]);
}
public void UpdateCells(World world, HashSet<CPos> dirtyCells)
{
var neighborDomains = new List<int>();
foreach (var cell in dirtyCells)
{
// Select all neighbors inside the map boundaries
var thisCell = cell; // benign closure hazard
var neighbors = CVec.Directions.Select(d => d + thisCell)
.Where(c => map.Contains(c));
var found = false;
foreach (var n in neighbors)
{
if (!dirtyCells.Contains(n))
{
var neighborDomain = domains[n];
if (CanTraverseTile(world, n))
{
neighborDomains.Add(neighborDomain);
// Set ourselves to the first non-dirty neighbor we find.
if (!found)
{
domains[cell] = neighborDomain;
found = true;
}
}
}
}
}
foreach (var c1 in neighborDomains)
foreach (var c2 in neighborDomains)
CreateConnection(c1, c2);
}
bool HasConnection(int d1, int d2)
{
// Search our connections graph for a possible route
var visited = new HashSet<int>();
var toProcess = new Stack<int>();
toProcess.Push(d1);
while (toProcess.Any())
{
var current = toProcess.Pop();
if (!transientConnections.ContainsKey(current))
continue;
foreach (var neighbor in transientConnections[current])
{
if (neighbor == d2)
return true;
if (!visited.Contains(neighbor))
toProcess.Push(neighbor);
}
visited.Add(current);
}
return false;
}
void CreateConnection(int d1, int d2)
{
if (!transientConnections.ContainsKey(d1))
transientConnections[d1] = new HashSet<int>();
if (!transientConnections.ContainsKey(d2))
transientConnections[d2] = new HashSet<int>();
transientConnections[d1].Add(d2);
transientConnections[d2].Add(d1);
}
bool CanTraverseTile(World world, CPos p)
{
if (!map.Contains(p))
return false;
var terrainOffset = world.Map.GetTerrainIndex(p);
return (movementClass & (1 << terrainOffset)) > 0;
}
void BuildDomains(World world)
{
var domain = 1;
var visited = new CellLayer<bool>(map);
var toProcess = new Queue<CPos>();
toProcess.Enqueue(MPos.Zero.ToCPos(map));
// 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])
continue;
var domainQueue = new Queue<CPos>();
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])
continue;
var candidatePassable = CanTraverseTile(world, n);
if (candidatePassable != currentPassable)
{
toProcess.Enqueue(n);
continue;
}
visited[n] = true;
domains[n] = domain;
// Don't crawl off the map, or add already-visited cells
var neighbors = CVec.Directions.Select(d => n + d)
.Where(p => visited.Contains(p) && !visited[p]);
foreach (var neighbor in neighbors)
domainQueue.Enqueue(neighbor);
}
domain += 1;
}
Log.Write("debug", "Found {0} domains for movement class {1} on map {2}.", domain - 1, movementClass, map.Title);
}
}
}