#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(); } } /// Get the WPos of the -X-Y corner of a CPos cell. public static WPos CornerToWPos(CPos xy, MapGridType gridType) { switch (gridType) { case MapGridType.Rectangular: return new WPos( xy.X * 1024, xy.Y * 1024, 0); case MapGridType.RectangularIsometric: return new WPos( (xy.X - xy.Y) * 724 + 724, (xy.X + xy.Y) * 724, 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 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(); } } /// /// 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, int 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 / 1024 + 1; mRadiusV = wRadius / 1024 + 1; break; case MapGridType.RectangularIsometric: mRadiusU = wRadius / 1448 + 2; mRadiusV = wRadius / 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 = (long)wRadius * wRadius; 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 matrix cells such that the given quantile, /// fraction, has the given target value. /// public static void CalibrateQuantileInPlace(CellLayer cellLayer, float target, float fraction) { var sorted = Entries(cellLayer); Array.Sort(sorted); var adjustment = target - MatrixUtils.ArrayQuantile(sorted, fraction); foreach (var mpos in cellLayer.CellRegion.MapCoords) cellLayer[mpos] += adjustment; } /// /// 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(new Size(map.MapSize.X, map.MapSize.Y), 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. public static void WalkingDistances( CellLayer distances, CellLayer passable, IEnumerable seeds, float 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 next = seeds.ToList(); while (next.Count != 0) { var current = next; next = new List<(CPos, P)>(); 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)); } } } } } }