#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * 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, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using OpenRA.Primitives; using OpenRA.Support; namespace OpenRA.Mods.Common.MapGenerator { public static class CellLayerUtils { static int FloorDiv(int a, int b) { var q = Math.DivRem(a, b, out var r); if (r < 0) return q - 1; else return q; } /// Return true iff a and b have the same grid type and size. public static bool AreSameShape(CellLayer a, CellLayer b) { return a.Size == b.Size && a.GridType == b.GridType; } /// /// Returns the half-way point between the centers of the top-left (first) and bottom-right /// (last) MPos cells. This will either lie in the exact center of a cell, an edge between /// two cells, or a corner between four cells. Note that this might not fit all reasonable /// or intuitive definitions of a map center, but has convenient properties. /// public static WPos Center(CellLayer cellLayer) { switch (cellLayer.GridType) { case MapGridType.Rectangular: return new WPos( cellLayer.Size.Width * 512, cellLayer.Size.Height * 512, 0); case MapGridType.RectangularIsometric: return new WPos( (cellLayer.Size.Width * 2 + (~cellLayer.Size.Height & 1)) * 362, (cellLayer.Size.Height + 1) * 362, 0); default: throw new NotImplementedException(); } } public static WPos Center(Map map) { return Center(map.Tiles); } /// /// Return the radius of the largest circle that can be contained in the cell layer. /// public static WDist Radius(CellLayer cellLayer) { var center = Center(cellLayer); return new WDist(Math.Min(center.X, center.Y)); } public static WDist Radius(Map map) { return Radius(map.Tiles); } /// Get the WPos of the -X-Y corner of a CPos cell. public static WPos CornerToWPos(CPos cpos, MapGridType gridType) { switch (gridType) { case MapGridType.Rectangular: return new WPos( cpos.X * 1024, cpos.Y * 1024, 0); case MapGridType.RectangularIsometric: return new WPos( (cpos.X - cpos.Y) * 724 + 724, (cpos.X + cpos.Y) * 724, 0); default: throw new NotImplementedException(); } } /// Get the closest -X-Y corner of a CPos cell to a WPos. public static CPos WPosToCorner(WPos wpos, MapGridType gridType) { switch (gridType) { case MapGridType.Rectangular: return new CPos( FloorDiv(wpos.X + 512, 1024), FloorDiv(wpos.Y + 512, 1024), 0); case MapGridType.RectangularIsometric: return new CPos( FloorDiv(wpos.Y + wpos.X, 1448), FloorDiv(wpos.Y - wpos.X + 1448, 1448), 0); default: throw new NotImplementedException(); } } /// Get the WVec representing the same translation as the given CVec. public static WVec CVecToWVec(CVec cvec, MapGridType gridType) { switch (gridType) { case MapGridType.Rectangular: return new WVec( cvec.X * 1024, cvec.Y * 1024, 0); case MapGridType.RectangularIsometric: return new WVec( (cvec.X - cvec.Y) * 724, (cvec.X + cvec.Y) * 724, 0); default: throw new NotImplementedException(); } } /// Get the WVec representing the same translation as the given CVec, with a height offset. public static WVec CVecToWVec(CVec cvec, int dz, MapGridType gridType) { switch (gridType) { case MapGridType.Rectangular: return new WVec( cvec.X * 1024, cvec.Y * 1024, 0); case MapGridType.RectangularIsometric: return new WVec( (cvec.X - cvec.Y) * 724, (cvec.X + cvec.Y) * 724, 724 * dz); default: throw new NotImplementedException(); } } /// Get the WPos center of a CPos cell. public static WPos CPosToWPos(CPos cpos, MapGridType gridType) { var wvec = CVecToWVec(new CVec(cpos.X, cpos.Y), gridType); switch (gridType) { case MapGridType.Rectangular: return new WPos(512, 512, 0) + wvec; case MapGridType.RectangularIsometric: return new WPos(724, 724, 0) + wvec; default: throw new NotImplementedException(); } } /// Get the WPos center of a CPos cell with a height offset. public static WPos CPosToWPos(CPos cpos, int dz, MapGridType gridType) { var wvec = CVecToWVec(new CVec(cpos.X, cpos.Y), dz, gridType); switch (gridType) { case MapGridType.Rectangular: return new WPos(512, 512, 0) + wvec; case MapGridType.RectangularIsometric: return new WPos(724, 724, 0) + wvec; default: throw new NotImplementedException(); } } /// /// Find the CPos cell in which a WPos position lies. WPos positions on /// an edge or corner match the CPos with higher X and/or Y positions. /// public static CPos WPosToCPos(WPos wpos, MapGridType gridType) { switch (gridType) { case MapGridType.Rectangular: return new CPos( FloorDiv(wpos.X, 1024), FloorDiv(wpos.Y, 1024), 0); case MapGridType.RectangularIsometric: return new CPos( FloorDiv(wpos.Y + wpos.X - 724, 1448), FloorDiv(wpos.Y - wpos.X + 724, 1448), 0); default: throw new NotImplementedException(); } } /// Get the WPos center of an MPos cell. public static WPos MPosToWPos(MPos mpos, MapGridType gridType) { return CPosToWPos(mpos.ToCPos(gridType), gridType); } /// /// Find the MPos cell in which a WPos position lies. WPos positions on /// an edge or corner match the CPos (not necessarily MPos) with higher /// X and/or Y positions. /// public static MPos WPosToMPos(WPos wpos, MapGridType gridType) { return WPosToCPos(wpos, gridType).ToMPos(gridType); } /// /// Translates CPos-like positions to zero-based positions. /// public static int2[][] ToMatrixPoints( IEnumerable pointArrayArray, CellLayer cellLayer) { var cellBounds = CellBounds(cellLayer); return pointArrayArray .Select(xys => xys .Select(xy => new int2(xy.X - cellBounds.Left, xy.Y - cellBounds.Top)) .ToArray()) .ToArray(); } /// /// Translates zero-based positions to CPos-like positions. /// public static CPos[][] FromMatrixPoints( IEnumerable pointArrayArray, CellLayer cellLayer) { var cellBounds = CellBounds(cellLayer); return pointArrayArray .Select(xys => xys .Select(xy => new CPos(xy.X + cellBounds.Left, xy.Y + cellBounds.Top)) .ToArray()) .ToArray(); } /// /// /// Run an action over the inside or outside of a circle of given center and radius in /// world coordinates. The action is called with cells' MPos, CPos, WPos center, and the /// squared distance to the WPos center from the circle's center. /// If outside is true, the action is run for cells outside of the circle instead of the /// inside. /// /// /// A cell is inside the circle if its center is <= wRadius from wCenter. /// Coordinates outside of the CellLayer are ignored. /// /// public static void OverCircle( CellLayer cellLayer, WPos wCenter, WDist wRadius, bool outside, Action action) { var gridType = cellLayer.GridType; int minU; int minV; int maxU; int maxV; if (outside) { minU = 0; minV = 0; maxU = cellLayer.Size.Width - 1; maxV = cellLayer.Size.Height - 1; } else { var mCenter = WPosToMPos(wCenter, gridType); int mRadiusU; int mRadiusV; switch (gridType) { case MapGridType.Rectangular: mRadiusU = wRadius.Length / 1024 + 1; mRadiusV = wRadius.Length / 1024 + 1; break; case MapGridType.RectangularIsometric: mRadiusU = wRadius.Length / 1448 + 2; mRadiusV = wRadius.Length / 724 + 2; break; default: throw new NotImplementedException(); } minU = Math.Max(mCenter.U - mRadiusU, 0); minV = Math.Max(mCenter.V - mRadiusV, 0); maxU = Math.Min(mCenter.U + mRadiusU, cellLayer.Size.Width - 1); maxV = Math.Min(mCenter.V + mRadiusV, cellLayer.Size.Height - 1); } var wRadiusSquared = wRadius.LengthSquared; for (var v = minV; v <= maxV; v++) for (var u = minU; u <= maxU; u++) { var mpos = new MPos(u, v); var cpos = mpos.ToCPos(gridType); var wpos = CPosToWPos(cpos, gridType); var offset = wCenter - wpos; var thisRadiusSquared = offset.LengthSquared; if (thisRadiusSquared <= wRadiusSquared != outside) action(mpos, cpos, wpos, thisRadiusSquared); } } /// /// Return a linear copy of all entries in a CellLayer, ordered v * width + u, similar to /// MPos(0, 0), MPos(1, 0), MPos(2, 0), ..., MPos(0, 1), MPos(1, 1), MPos(2, 1), ... /// public static T[] Entries(CellLayer cellLayer) { var i = 0; var entries = new T[cellLayer.Size.Width * cellLayer.Size.Height]; foreach (var value in cellLayer) entries[i++] = value; return entries; } /// /// Uniformally add to or subtract from all cells such that the quantile (count/outOf) has at the target value. /// For example, (target: 0, count: 25, outOf: 75) where there are 401 cells would mean /// that 100 cells are no greater than 0, 300 cells are no less than 0, and at least 1 cell /// is 0. /// public static void CalibrateQuantileInPlace(CellLayer cellLayer, int target, int count, int outOf) { var sorted = Entries(cellLayer); Array.Sort(sorted); var adjustment = target - sorted[(long)(sorted.Length - 1) * count / outOf]; foreach (var mpos in cellLayer.CellRegion.MapCoords) cellLayer[mpos] += adjustment; } /// /// Return a boolean CellLayer where true correlates with the largest values in the input, /// such that the fraction of true cells is at least (but approximately) count/outOf. /// public static CellLayer CalibratedBooleanThreshold(CellLayer input, int count, int outOf) { var output = new CellLayer(input.GridType, input.Size); if (count <= 0) { return output; } else if (count >= outOf) { output.Clear(true); return output; } var sorted = Entries(input); Array.Sort(sorted); var threshold = sorted[(long)sorted.Length * (outOf - count) / outOf]; foreach (var mpos in input.CellRegion.MapCoords) output[mpos] = input[mpos] >= threshold; return output; } /// /// Get the smallest CPos rectangle that contains all cells for the specified grid. /// public static Rectangle CellBounds(Size size, MapGridType gridType) { switch (gridType) { case MapGridType.Rectangular: return new Rectangle(0, 0, size.Width, size.Height); case MapGridType.RectangularIsometric: { var maxCX = new MPos(size.Width - 1, size.Height - 1) .ToCPos(gridType).X; var minCY = new MPos(size.Width - 1, 0) .ToCPos(gridType).Y; var maxCY = new MPos(0, size.Height - 1) .ToCPos(gridType).Y; return Rectangle.FromLTRB(0, minCY, maxCX + 1, maxCY + 1); } default: throw new NotImplementedException(); } } /// /// Get the smallest CPos rectangle that contains all cells in a CellLayer. /// public static Rectangle CellBounds(CellLayer cellLayer) { return CellBounds(cellLayer.Size, cellLayer.GridType); } /// /// Get the smallest CPos rectangle that contains all cells in a map. /// public static Rectangle CellBounds(Map map) { return CellBounds(map.MapSize, map.Grid.Type); } /// /// Copies a CPos-aligned Matrix into a CellLayer. Depending on the grid type, this may /// discard data for cells that don't exist in the CellLayer. /// public static void FromMatrix( CellLayer cellLayer, Matrix matrix, bool allowOversizedMatrix = false) { var cellBounds = CellBounds(cellLayer); var size = cellBounds.Size.ToInt2(); if (allowOversizedMatrix) { if (matrix.Size.X < size.X || matrix.Size.Y < size.Y) throw new ArgumentException("source Matrix does not cover destination CellLayer"); } else if (matrix.Size != size) { throw new ArgumentException("destination and source have incompatible sizes"); } foreach (var cpos in cellLayer.CellRegion) cellLayer[cpos] = matrix[cpos.X - cellBounds.Left, cpos.Y - cellBounds.Top]; } /// /// Copies a CellLayer into a CPos-aligned Matrix. Depending on the grid type, this may /// fill the matrix with some default values for cells that don't exist in the CellLayer. /// public static Matrix ToMatrix(CellLayer cellLayer, T defaultValue) { var cellBounds = CellBounds(cellLayer); var matrix = new Matrix(cellBounds.Size.ToInt2()).Fill(defaultValue); foreach (var cpos in cellLayer.CellRegion) matrix[cpos.X - cellBounds.Left, cpos.Y - cellBounds.Top] = cellLayer[cpos]; return matrix; } /// Wrapper around MatrixUtils.BordersToPoints in CPos space. public static CPos[][] BordersToPoints(CellLayer cellLayer, CellLayer mask = null) { if (mask != null) { if (!AreSameShape(cellLayer, mask)) throw new ArgumentException("cellLayer and mask must have same shape."); } else { mask = new CellLayer(cellLayer.GridType, cellLayer.Size); mask.Clear(true); } var matrix = ToMatrix(cellLayer, false); var maskMatrix = ToMatrix(mask, false); var matrixPoints = MatrixUtils.BordersToPoints(matrix, maskMatrix); return FromMatrixPoints(matrixPoints, cellLayer); } /// Wrapper around MatrixUtils.ChebyshevRoom in CPos space. public static void ChebyshevRoom( CellLayer output, CellLayer input, bool outsideValue) { var matrix = ToMatrix(input, outsideValue); var roominess = MatrixUtils.ChebyshevRoom(matrix, outsideValue); FromMatrix(output, roominess); } /// /// Wrapper around MatrixUtils.WalkingDistance in CPos space. /// Returns world distances (1024ths). /// public static void WalkingDistances( CellLayer distances, CellLayer passable, IEnumerable seeds, WDist maxDistance) { var passableMatrix = ToMatrix(passable, false); var cellBounds = CellBounds(passable); var int2Seeds = seeds .Select(cpos => new int2(cpos.X - cellBounds.Left, cpos.Y - cellBounds.Top)); var distancesMatrix = MatrixUtils.WalkingDistances(passableMatrix, int2Seeds, maxDistance); FromMatrix(distances, distancesMatrix); } /// /// Rank all cell values and select the best (greatest compared) value. /// If there are equally good best candidates, choose one at random. /// public static (MPos MPos, T Value) FindRandomBest( CellLayer cellLayer, MersenneTwister random, Comparison comparison) { var candidates = new List(); var best = cellLayer[new MPos(0, 0)]; foreach (var mpos in cellLayer.CellRegion.MapCoords) { var rank = comparison(cellLayer[mpos], best); if (rank > 0) { best = cellLayer[mpos]; candidates.Clear(); } if (rank >= 0) candidates.Add(mpos); } var choice = candidates[random.Next(candidates.Count)]; return (choice, best); } /// /// Pick a random MPos position in a CellLayer where each cell is a /// selection weight. /// public static MPos PickWeighted(CellLayer weights, MersenneTwister random) { var entries = Entries(weights); var choice = random.PickWeighted(entries); var v = Math.DivRem(choice, weights.Size.Width, out var u); return new MPos(u, v); } /// /// /// Perform a generic flood fill starting at seeds [(cpos, prop), ...]. /// /// /// For each point being considered for fill, filler(cpos, prop) is /// called with the current position (cpos) and propagation value (prop). /// filler should return the value to be propagated or null if not to be /// propagated. Propagation happens to all neighbours (offsets) defined /// by spread, regardless of whether they have previously been visited, /// so filler is responsible for terminating propagation by returning /// nulls. Usually, Direction.Spread4CVec or Direction.Spread8CVec /// is appropriate as a spread pattern. /// /// /// filler should capture and manipulate any necessary input and output /// arrays. /// /// /// Each call to filler will have either an equal or greater /// growth/propagation distance from their seed value than all calls /// before it. (You can think of this as them being called in ordered /// growth layers.) /// /// /// Note that filler may be called multiple times for the same spot, /// perhaps with different propagation values. Within the same /// growth/propagation distance, filler will be called from values /// propagated from earlier seeds before values propagated from later /// seeds. /// /// /// filler is not called for positions outside of cellLayer EXCEPT for /// points being processed as seed values. /// /// public static void FloodFill( CellLayer cellLayer, IEnumerable<(CPos CPos, P Prop)> seeds, Func filler, ImmutableArray spread) where P : struct { var current = new List<(CPos CPos, P Prop)>(); var next = seeds.ToList(); while (next.Count != 0) { (next, current) = (current, next); next.Clear(); foreach (var (source, prop) in current) { var newProp = filler(source, prop); if (newProp != null) foreach (var offset in spread) { var destination = source + offset; if (cellLayer.Contains(destination)) next.Add((destination, (P)newProp)); } } } } /// /// Simple flood fill that propagates, starting from seed cells, throughout a masked area. /// The fillAction is run once (in a consistent order) for each filled cell. /// public static void SimpleFloodFill( CellLayer mask, CellLayer seeds, Action fillAction, ImmutableArray spread) { if (!AreSameShape(mask, seeds)) throw new ArgumentException("mask and seeds did not have same shape"); var available = Clone(mask); bool? Filler(CPos cpos, bool _) { if (!available[cpos]) return null; fillAction(cpos); available[cpos] = false; return true; } FloodFill( available, seeds.CellRegion .Where(cpos => seeds[cpos] && mask[cpos]) .Select(cpos => (cpos, true)), Filler, spread); } /// Return logical AND / conjunction / intersection of layers. public static CellLayer Intersect(IEnumerable> layers) { return Aggregate(layers, (a, b) => a && b); } /// /// Return the difference of layers. Each cell is true if and only if something appears /// only in the first layer. /// public static CellLayer Subtract(IEnumerable> layers) { return Aggregate(layers, (a, b) => a && !b); } public static CellLayer Aggregate( IEnumerable> layers, Func aggregator) { var layersArray = layers.ToArray(); if (layersArray.Length == 0) throw new ArgumentException("No layers were supplied"); var accumulator = new CellLayer(layersArray[0].GridType, layersArray[0].Size); accumulator.CopyValuesFrom(layersArray[0]); foreach (var layer in layersArray.Skip(1)) { if (!AreSameShape(accumulator, layer)) throw new ArgumentException("Layers are not the same shape"); foreach (var mpos in accumulator.CellRegion.MapCoords) accumulator[mpos] = aggregator(accumulator[mpos], layer[mpos]); } return accumulator; } /// Create a shallow copy of a CellLayer. public static CellLayer Clone(CellLayer input) { var output = new CellLayer(input.GridType, input.Size); output.CopyValuesFrom(input); return output; } public static CellLayer Map(CellLayer input, Func func) { var output = new CellLayer(input.GridType, input.Size); foreach (var mpos in input.CellRegion.MapCoords) output[mpos] = func(input[mpos]); return output; } /// Create and initialize a CellLayer according to the given function. public static CellLayer Create(Map map, Func func) { var layer = new CellLayer(map); foreach (var mpos in map.AllCells.MapCoords) layer[mpos] = func(mpos); return layer; } /// Create and initialize a CellLayer according to the given function. public static CellLayer Create(Map map, Func func) { var layer = new CellLayer(map); foreach (var cpos in map.AllCells) layer[cpos] = func(cpos); return layer; } } }