#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;
}
}
}