Refactor ExperimentalMapGenerator into reusable methods
This change lifts large portions of ExperimentalMapGenerator's logic into a new "Terraformer" class with highly documented methods that can theoretically be used by alternative map generator classes. Some additional refactoring may occur in future, subject to the practical needs of additional map generators. ClearMapGenerator is also simplified. This is not a pure refactor and contains some algorithmic and behavioral changes, as well as few minor bug fixes. Notably: - The logic for obstructing unreachable water has been somewhat replaced. - In CnC, where water is already unplayable, EMG no longer obstructs water. (RA still does, as water is playable there.) - Mountain (cliff) generation is now somewhat more effective. This increases the number of cliffs seen on many presets. Settings should no longer use "Mountains: 1000". - PlayableSpace logic has been consolidated into Terraformer. - PlayableSpace.Playability no longer exists as PartiallyPlayable became redundant. Its uses have been replaced with a simple boolean. Consequently, some defunct code and configuration has been removed. - Adjust MultiBrush replaceability contract painting behavior to be more intuitive. - Fix off-by-one error in map bounds computation. - Fix some usages of mixed up MersenneTwisters.
This commit is contained in:
committed by
Gustas Kažukauskas
parent
e4f289921c
commit
25571df2b6
@@ -145,5 +145,16 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
|
||||
return CellLayerUtils.CVecToWVec(new CVec(left + right, top + bottom), Map.Grid.Type) / 2;
|
||||
}
|
||||
|
||||
/// <summary>Return the larger of the width or height of the actor's footprint.</summary>
|
||||
public int MaxSpan()
|
||||
{
|
||||
var footprint = Footprint().Select(kv => kv.Key).ToList();
|
||||
var minX = footprint.Min(cpos => cpos.X);
|
||||
var minY = footprint.Min(cpos => cpos.Y);
|
||||
var maxX = footprint.Max(cpos => cpos.X);
|
||||
var maxY = footprint.Max(cpos => cpos.Y);
|
||||
return Math.Max(maxX - minX, maxY - minY) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,25 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
}
|
||||
}
|
||||
|
||||
public static WPos Center(Map map)
|
||||
{
|
||||
return Center(map.Tiles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the radius of the largest circle that can be contained in the cell layer.
|
||||
/// </summary>
|
||||
public static WDist Radius<T>(CellLayer<T> cellLayer)
|
||||
{
|
||||
var center = Center(cellLayer);
|
||||
return new WDist(Math.Min(center.X, center.Y));
|
||||
}
|
||||
|
||||
public static WDist Radius(Map map)
|
||||
{
|
||||
return Radius(map.Tiles);
|
||||
}
|
||||
|
||||
/// <summary>Get the WPos of the -X-Y corner of a CPos cell.</summary>
|
||||
public static WPos CornerToWPos(CPos cpos, MapGridType gridType)
|
||||
{
|
||||
@@ -218,7 +237,7 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
public static void OverCircle<T>(
|
||||
CellLayer<T> cellLayer,
|
||||
WPos wCenter,
|
||||
int wRadius,
|
||||
WDist wRadius,
|
||||
bool outside,
|
||||
Action<MPos, CPos, WPos, long> action)
|
||||
{
|
||||
@@ -243,12 +262,12 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
switch (gridType)
|
||||
{
|
||||
case MapGridType.Rectangular:
|
||||
mRadiusU = wRadius / 1024 + 1;
|
||||
mRadiusV = wRadius / 1024 + 1;
|
||||
mRadiusU = wRadius.Length / 1024 + 1;
|
||||
mRadiusV = wRadius.Length / 1024 + 1;
|
||||
break;
|
||||
case MapGridType.RectangularIsometric:
|
||||
mRadiusU = wRadius / 1448 + 2;
|
||||
mRadiusV = wRadius / 724 + 2;
|
||||
mRadiusU = wRadius.Length / 1448 + 2;
|
||||
mRadiusV = wRadius.Length / 724 + 2;
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
@@ -260,7 +279,7 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
maxV = Math.Min(mCenter.V + mRadiusV, cellLayer.Size.Height - 1);
|
||||
}
|
||||
|
||||
var wRadiusSquared = (long)wRadius * wRadius;
|
||||
var wRadiusSquared = wRadius.LengthSquared;
|
||||
for (var v = minV; v <= maxV; v++)
|
||||
for (var u = minU; u <= maxU; u++)
|
||||
{
|
||||
@@ -288,8 +307,10 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uniformally add to or subtract from all cells such that count out of every outOf cells,
|
||||
/// are no greater than the given target value.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static void CalibrateQuantileInPlace(CellLayer<int> cellLayer, int target, int count, int outOf)
|
||||
{
|
||||
@@ -300,6 +321,32 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
cellLayer[mpos] += adjustment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static CellLayer<bool> CalibratedBooleanThreshold(CellLayer<int> input, int count, int outOf)
|
||||
{
|
||||
var output = new CellLayer<bool>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the smallest CPos rectangle that contains all cells for the specified grid.
|
||||
/// </summary>
|
||||
@@ -419,10 +466,10 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
/// Returns world distances (1024ths).
|
||||
/// </summary>
|
||||
public static void WalkingDistances(
|
||||
CellLayer<int> distances,
|
||||
CellLayer<WDist> distances,
|
||||
CellLayer<bool> passable,
|
||||
IEnumerable<CPos> seeds,
|
||||
int maxDistance)
|
||||
WDist maxDistance)
|
||||
{
|
||||
var passableMatrix = ToMatrix(passable, false);
|
||||
var cellBounds = CellBounds(passable);
|
||||
@@ -532,5 +579,111 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static void SimpleFloodFill(
|
||||
CellLayer<bool> mask,
|
||||
CellLayer<bool> seeds,
|
||||
Action<CPos> fillAction,
|
||||
ImmutableArray<CVec> 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);
|
||||
}
|
||||
|
||||
/// <summary>Return logical AND / conjunction / intersection of layers.</summary>
|
||||
public static CellLayer<bool> Intersect(IEnumerable<CellLayer<bool>> layers)
|
||||
{
|
||||
return Aggregate(layers, (a, b) => a && b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the difference of layers. Each cell is true if and only if something appears
|
||||
/// only in the first layer.
|
||||
/// </summary>
|
||||
public static CellLayer<bool> Subtract(IEnumerable<CellLayer<bool>> layers)
|
||||
{
|
||||
return Aggregate(layers, (a, b) => a && !b);
|
||||
}
|
||||
|
||||
public static CellLayer<T> Aggregate<T>(
|
||||
IEnumerable<CellLayer<T>> layers,
|
||||
Func<T, T, T> aggregator)
|
||||
{
|
||||
var layersArray = layers.ToArray();
|
||||
if (layersArray.Length == 0)
|
||||
throw new ArgumentException("No layers were supplied");
|
||||
|
||||
var accumulator = new CellLayer<T>(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;
|
||||
}
|
||||
|
||||
/// <summary>Create a shallow copy of a CellLayer.</summary>
|
||||
public static CellLayer<T> Clone<T>(CellLayer<T> input)
|
||||
{
|
||||
var output = new CellLayer<T>(input.GridType, input.Size);
|
||||
output.CopyValuesFrom(input);
|
||||
return output;
|
||||
}
|
||||
|
||||
public static CellLayer<R> Map<T, R>(CellLayer<T> input, Func<T, R> func)
|
||||
{
|
||||
var output = new CellLayer<R>(input.GridType, input.Size);
|
||||
foreach (var mpos in input.CellRegion.MapCoords)
|
||||
output[mpos] = func(input[mpos]);
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>Create and initialize a CellLayer according to the given function.</summary>
|
||||
public static CellLayer<T> Create<T>(Map map, Func<MPos, T> func)
|
||||
{
|
||||
var layer = new CellLayer<T>(map);
|
||||
foreach (var mpos in map.AllCells.MapCoords)
|
||||
layer[mpos] = func(mpos);
|
||||
|
||||
return layer;
|
||||
}
|
||||
|
||||
/// <summary>Create and initialize a CellLayer according to the given function.</summary>
|
||||
public static CellLayer<T> Create<T>(Map map, Func<CPos, T> func)
|
||||
{
|
||||
var layer = new CellLayer<T>(map);
|
||||
foreach (var cpos in map.AllCells)
|
||||
layer[cpos] = func(cpos);
|
||||
|
||||
return layer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,164 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
{
|
||||
public const int MaxBinomialKernelRadius = 10;
|
||||
|
||||
public enum DumpAdjustment
|
||||
{
|
||||
/// <summary>Make no adjustment.</summary>
|
||||
None,
|
||||
|
||||
/// <summary>Normalize the matrix amplitude to the color range.</summary>
|
||||
Normalize,
|
||||
|
||||
/// <summary>
|
||||
/// Normalize the matrix amplitude, but uniformally extend away from zero by a small
|
||||
/// amount to help identify the sign of martix values.
|
||||
/// </summary>
|
||||
Emphasize,
|
||||
}
|
||||
|
||||
public enum GraphMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The plotted value is the latest sequence touching a cell + 1.
|
||||
/// </summary>
|
||||
Identifier,
|
||||
|
||||
/// <summary>
|
||||
/// The plotted value is the (latest) point index in the (latest) sequence touching a
|
||||
/// cell.
|
||||
/// </summary>
|
||||
Gradient,
|
||||
|
||||
/// <summary>The plotted value is the count of points touching a cell.</summary>
|
||||
Accumulate,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Debugging method that prints a matrix to stderr using color only (not value listing).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Orange < -255, -255 <= Red < 0, Black == 0, 0 < Blue <= 255,
|
||||
/// 255 < Cyan. Faint green is used for distance markings.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The matrix can optionally be preprocessed for easier visual interpretation using a
|
||||
/// DumpAdjustment.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static void ColorDump2d(
|
||||
string label,
|
||||
Matrix<int> matrix,
|
||||
DumpAdjustment adjustment = DumpAdjustment.None)
|
||||
{
|
||||
Console.Error.WriteLine($"{label}: {matrix.Size.X} by {matrix.Size.Y}, {matrix.Data.Min()} to {matrix.Data.Max()}");
|
||||
|
||||
switch (adjustment)
|
||||
{
|
||||
case DumpAdjustment.Normalize:
|
||||
matrix = NormalizeRangeInPlace(matrix.Clone(), 255);
|
||||
break;
|
||||
case DumpAdjustment.Emphasize:
|
||||
matrix = NormalizeRangeInPlace(matrix.Clone(), 224)
|
||||
.Map(v => v += Math.Sign(v) * 31);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
for (var y = 0; y < matrix.Size.Y; y++)
|
||||
{
|
||||
for (var x = 0; x < matrix.Size.X; x++)
|
||||
{
|
||||
var v = matrix[x, y];
|
||||
int r = 0, g = 0, b = 0;
|
||||
|
||||
if (v < -255)
|
||||
{
|
||||
r = 255;
|
||||
g = 192;
|
||||
}
|
||||
else if (v < 0)
|
||||
{
|
||||
r = -v;
|
||||
}
|
||||
else if (v == 0)
|
||||
{
|
||||
}
|
||||
else if (v <= 255)
|
||||
{
|
||||
b = v;
|
||||
g = v / 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
// v > 255
|
||||
b = 255;
|
||||
g = 192;
|
||||
}
|
||||
|
||||
g += (((x & 4) != (y & 4)) ? 1 : 0) * (((x & 16) != (y & 16)) ? 48 : 32);
|
||||
|
||||
Console.Error.Write(string.Format(NumberFormatInfo.InvariantInfo, "\u001b[48;2;{0};{1};{2}m ", r, g, b));
|
||||
}
|
||||
|
||||
Console.Error.Write("\u001b[0m\n");
|
||||
}
|
||||
|
||||
Console.Error.WriteLine("");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
public static void ColorDump2d(
|
||||
string label,
|
||||
Matrix<bool> matrix)
|
||||
{
|
||||
ColorDump2d(label, matrix.Map(v => v ? 255 : -255));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Debugging method that prints a matrix of enum-like values to stderr, where values are
|
||||
/// mapped to one of 27 different colors. Red, green, and blue values represent base-3
|
||||
/// digits of increasing significance. Unmappable values produce white. A corresponding
|
||||
/// letter of the latin alphabet is also written in the right of cells greater than zero.
|
||||
/// E.g., 21_base10 = 210_base3 = bright blue + medium green + no red, letter U.
|
||||
/// </summary>
|
||||
public static void EnumDump2d(string label, Matrix<int> matrix)
|
||||
{
|
||||
Console.Error.WriteLine($"{label}: {matrix.Size.X} by {matrix.Size.Y}, {matrix.Data.Min()} to {matrix.Data.Max()}");
|
||||
for (var y = 0; y < matrix.Size.Y; y++)
|
||||
{
|
||||
for (var x = 0; x < matrix.Size.X; x++)
|
||||
{
|
||||
var v = matrix[x, y];
|
||||
if (v < 0 || v > 26)
|
||||
v = 26;
|
||||
|
||||
var r = 127 * (v / 1 % 3);
|
||||
var g = 127 * (v / 3 % 3);
|
||||
var b = 127 * (v / 9 % 3);
|
||||
var f = (r + g + b <= 127) ? 37 : 30;
|
||||
var c = v > 0 ? (char)(64 + v) : '.';
|
||||
Console.Error.Write(string.Format(NumberFormatInfo.InvariantInfo, "\u001b[{0};48;2;{1};{2};{3}m {4}", f, r, g, b, c));
|
||||
|
||||
// if (v < 0 || v >= 15)
|
||||
// v = 15;
|
||||
// var code = (v < 8 ? 40 : 92) + v;
|
||||
// Console.Error.Write(string.Format(NumberFormatInfo.InvariantInfo, "\u001b[{0}m .", code));
|
||||
}
|
||||
|
||||
Console.Error.Write("\u001b[0m\n");
|
||||
}
|
||||
|
||||
Console.Error.WriteLine("");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
public static void EnumDump2d<T>(string label, Matrix<T> matrix) where T : Enum
|
||||
{
|
||||
EnumDump2d(label, matrix.Map(v => Convert.ToInt32(v, NumberFormatInfo.InvariantInfo)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Debugging method that prints a matrix to stderr.
|
||||
/// </summary>
|
||||
@@ -96,6 +254,55 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plot multiple point sequences onto a matrix for debugging visualization. The matrix is
|
||||
/// fit to the shape of all the path.
|
||||
/// </summary>
|
||||
public static Matrix<int> GraphPoints(
|
||||
IEnumerable<IEnumerable<int2>> pointArrays,
|
||||
GraphMode mode = GraphMode.Identifier)
|
||||
{
|
||||
var pointArrayArray = pointArrays.Select(a => a.ToArray()).ToArray();
|
||||
var allPoints = pointArrayArray.SelectMany(p => p).ToArray();
|
||||
if (allPoints.Length == 0)
|
||||
return new Matrix<int>(1, 1).Fill(int.MinValue);
|
||||
|
||||
var topLeft = new int2(allPoints.Min(p => p.X), allPoints.Min(p => p.Y));
|
||||
var bottomRight = new int2(allPoints.Max(p => p.X), allPoints.Max(p => p.Y));
|
||||
var size = bottomRight - topLeft + new int2(1, 1);
|
||||
var matrix = new Matrix<int>(size).Fill(mode == GraphMode.Gradient ? -1 : 0);
|
||||
for (var j = 0; j < pointArrayArray.Length; j++)
|
||||
{
|
||||
var pointArray = pointArrayArray[j];
|
||||
for (var i = 0; i < pointArray.Length; i++)
|
||||
switch (mode)
|
||||
{
|
||||
case GraphMode.Identifier:
|
||||
matrix[pointArray[i] - topLeft] = j + 1;
|
||||
break;
|
||||
case GraphMode.Gradient:
|
||||
matrix[pointArray[i] - topLeft] = i;
|
||||
break;
|
||||
case GraphMode.Accumulate:
|
||||
matrix[pointArray[i] - topLeft]++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plot a point sequence onto a matrix for debugging visualization. The matrix is fit to
|
||||
/// the shape of the path.
|
||||
/// </summary>
|
||||
public static Matrix<int> GraphPoints(
|
||||
IEnumerable<int2> points,
|
||||
GraphMode mode = GraphMode.Identifier)
|
||||
{
|
||||
return GraphPoints([points], mode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Perform a generic flood fill starting at seeds <c>[(xy, prop), ...]</c>.
|
||||
@@ -166,12 +373,12 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
/// int.MaxValue.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static Matrix<int> WalkingDistances(Matrix<bool> passable, IEnumerable<int2> seeds, int maxDistance)
|
||||
public static Matrix<WDist> WalkingDistances(Matrix<bool> passable, IEnumerable<int2> seeds, WDist maxDistance)
|
||||
{
|
||||
const int Diagonal = 1448;
|
||||
const int Straight = 1024;
|
||||
|
||||
var output = new Matrix<int>(passable.Size).Fill(int.MaxValue);
|
||||
var output = new Matrix<WDist>(passable.Size).Fill(WDist.MaxValue);
|
||||
var unprocessed = new PriorityArray<int>(passable.Size.X * passable.Size.Y, int.MaxValue);
|
||||
foreach (var seed in seeds)
|
||||
unprocessed[passable.Index(seed)] = 0;
|
||||
@@ -182,11 +389,11 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
var distance = unprocessed[i];
|
||||
var xy = passable.XY(i);
|
||||
|
||||
if (distance > maxDistance)
|
||||
if (distance > maxDistance.Length)
|
||||
break;
|
||||
|
||||
if (distance <= maxDistance && output.ContainsXY(xy))
|
||||
output[xy] = distance;
|
||||
if (distance <= maxDistance.Length && output.ContainsXY(xy))
|
||||
output[xy] = new WDist(distance);
|
||||
unprocessed[i] = int.MaxValue;
|
||||
|
||||
foreach (var (offset, direction) in DirectionExts.Spread8D)
|
||||
@@ -196,7 +403,7 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
continue;
|
||||
if (!passable[nextXY])
|
||||
continue;
|
||||
if (output[nextXY] != int.MaxValue)
|
||||
if (output[nextXY] != WDist.MaxValue)
|
||||
continue;
|
||||
int nextDistance;
|
||||
if (direction.IsDiagonal())
|
||||
@@ -712,8 +919,10 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uniformally add to or subtract from all cells such that count out of every outOf cells,
|
||||
/// are no greater than the given target value.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static void CalibrateQuantileInPlace(Matrix<int> matrix, int target, int count, int outOf)
|
||||
{
|
||||
@@ -724,6 +933,23 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
matrix[i] += adjustment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a boolean matrix where true correlates with the largest values in the input,
|
||||
/// such that the fraction of true cells is at least (but approximately) count/outOf.
|
||||
/// </summary>
|
||||
public static Matrix<bool> CalibratedBooleanThreshold(Matrix<int> input, int count, int outOf)
|
||||
{
|
||||
if (count <= 0)
|
||||
return new Matrix<bool>(input.Size);
|
||||
else if (count >= outOf)
|
||||
return new Matrix<bool>(input.Size).Fill(true);
|
||||
|
||||
var sorted = (int[])input.Data.Clone();
|
||||
Array.Sort(sorted);
|
||||
var threshold = sorted[(long)sorted.Length * (outOf - count) / outOf];
|
||||
return input.Map(v => v >= threshold);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For true cells, gives the Chebyshev distance to the closest false cell.
|
||||
/// For false cells, gives the Chebyshev distance to the closest true cell as a negative.
|
||||
@@ -785,13 +1011,16 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
/// <para>
|
||||
/// Given a set of grid-intersection point arrays, creates a matrix where each cell
|
||||
/// identifies whether the closest points are wrapping around it clockwise or
|
||||
/// counter-clockwise (as defined in MapUtils.Direction).
|
||||
/// counter-clockwise (as defined in MapGenerator.Direction).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Positive output values indicate the points are wrapping around it clockwise.
|
||||
/// Negative output values indicate the points are wrapping around it counter-clockwise.
|
||||
/// Outputs can be zero or non-unit magnitude if there are fighting point arrays.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If no points are on or close enough to the matrix area, returns null.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static Matrix<int> PointsChirality(int2 size, IEnumerable<int2[]> pointArrayArray)
|
||||
{
|
||||
@@ -809,6 +1038,7 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
}
|
||||
|
||||
foreach (var pointArray in pointArrayArray)
|
||||
{
|
||||
for (var i = 1; i < pointArray.Length; i++)
|
||||
{
|
||||
var from = pointArray[i - 1];
|
||||
@@ -838,6 +1068,10 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
throw new ArgumentException("Unsupported direction for chirality");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (seeds.Count == 0)
|
||||
return null;
|
||||
|
||||
int? FillChirality(int2 point, int prop)
|
||||
{
|
||||
|
||||
@@ -538,22 +538,24 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
case Replaceability.None:
|
||||
throw new ArgumentException("Cannot paint: Replaceability.None");
|
||||
case Replaceability.Any:
|
||||
if (this.actorPlans.Count > 0)
|
||||
PaintActors(map, actorPlans, paintAt);
|
||||
else if (tiles.Count > 0)
|
||||
PaintTiles(map, paintAt, random);
|
||||
else
|
||||
if (this.actorPlans.Count == 0 && tiles.Count == 0)
|
||||
throw new ArgumentException("Cannot paint: no tiles or actors");
|
||||
|
||||
PaintTiles(map, paintAt, random);
|
||||
PaintActors(map, actorPlans, paintAt);
|
||||
|
||||
break;
|
||||
case Replaceability.Tile:
|
||||
if (tiles.Count == 0)
|
||||
throw new ArgumentException("Cannot paint: no tiles");
|
||||
|
||||
PaintTiles(map, paintAt, random);
|
||||
PaintActors(map, actorPlans, paintAt);
|
||||
break;
|
||||
case Replaceability.Actor:
|
||||
if (this.actorPlans.Count == 0)
|
||||
throw new ArgumentException("Cannot paint: no actors");
|
||||
|
||||
PaintActors(map, actorPlans, paintAt);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,24 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
const int Scale = 1024;
|
||||
const int ScaledSqrt2 = 1448;
|
||||
|
||||
/// <summary>Amplitude is the same for all wavelengths.</summary>
|
||||
public static int WhiteAmplitude(int wavelength) => 1;
|
||||
|
||||
/// <summary>Amplitude proportional to wavelength.</summary>
|
||||
public static int PinkAmplitude(int wavelength) => wavelength;
|
||||
|
||||
/// <summary>
|
||||
/// <code>amplitude = wavelength ** (1 / (2 ** clumpiness))</code>
|
||||
/// Setting clumpiness to 0 is equivalent to pink noise.
|
||||
/// </summary>
|
||||
public static int ClumpinessAmplitude(int wavelength, int clumpiness)
|
||||
{
|
||||
var amplitude = wavelength;
|
||||
for (var i = 0; i < clumpiness; i++)
|
||||
amplitude = Exts.ISqrt(amplitude);
|
||||
return amplitude;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Create noise by combining multiple layers of Perlin noise of halving wavelengths.
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
#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.Collections.Generic;
|
||||
|
||||
namespace OpenRA.Mods.Common.MapGenerator
|
||||
{
|
||||
public static class PlayableSpace
|
||||
{
|
||||
public enum Playability
|
||||
{
|
||||
/// <summary>Area is unplayable by land/naval units.</summary>
|
||||
Unplayable = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Area is unplayable by land/naval units, but should count as
|
||||
/// being "within" a playable region. This usually applies to random
|
||||
/// rock or river tiles in largely passable templates.
|
||||
/// </summary>
|
||||
Partial = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Area is playable by either land or naval units.
|
||||
/// </summary>
|
||||
Playable = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Additional data for a region containing playable space.
|
||||
/// The shape of a region is specified separately via a region mask.
|
||||
/// </summary>
|
||||
public sealed class Region
|
||||
{
|
||||
/// <summary>Area of playable and partially playable space.</summary>
|
||||
public int Area;
|
||||
|
||||
/// <summary>Area of fully playable space.</summary>
|
||||
public int PlayableArea;
|
||||
|
||||
/// <summary>Region ID.</summary>
|
||||
public int Id;
|
||||
}
|
||||
|
||||
/// <summary>Sentinel indicating a position isn't assigned to a region.</summary>
|
||||
public const int NullRegion = -1;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Analyses a given map's tiles and ActorPlans and determines the playable space within it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Requires a playabilityMap which specifies whether certain tiles are considered playable
|
||||
/// or not. Actors are always considered partially playable.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// RegionMap contains the mapping of map positions to Regions. If a map position is not
|
||||
/// within a region, the value is NullRegion.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static (Region[] Regions, CellLayer<int> RegionMap, CellLayer<Playability> Playable) FindPlayableRegions(
|
||||
Map map,
|
||||
List<ActorPlan> actorPlans,
|
||||
Dictionary<TerrainTile, Playability> playabilityMap)
|
||||
{
|
||||
var regions = new List<Region>();
|
||||
var regionMap = new CellLayer<int>(map);
|
||||
regionMap.Clear(NullRegion);
|
||||
var playable = new CellLayer<Playability>(map);
|
||||
playable.Clear(Playability.Unplayable);
|
||||
foreach (var mpos in map.AllCells.MapCoords)
|
||||
if (map.Contains(mpos))
|
||||
playable[mpos] = playabilityMap[map.Tiles[mpos]];
|
||||
|
||||
foreach (var actorPlan in actorPlans)
|
||||
foreach (var cpos in actorPlan.Footprint().Keys)
|
||||
if (map.AllCells.Contains(cpos))
|
||||
{
|
||||
var mpos = cpos.ToMPos(map);
|
||||
if (playable[mpos] == Playability.Playable)
|
||||
playable[mpos] = Playability.Partial;
|
||||
}
|
||||
|
||||
void Fill(Region region, CPos start)
|
||||
{
|
||||
void AddToRegion(CPos cpos, bool fullyPlayable)
|
||||
{
|
||||
var mpos = cpos.ToMPos(map);
|
||||
regionMap[mpos] = region.Id;
|
||||
region.Area++;
|
||||
if (fullyPlayable)
|
||||
region.PlayableArea++;
|
||||
}
|
||||
|
||||
bool? Filler(CPos cpos, bool fullyPlayable)
|
||||
{
|
||||
var mpos = cpos.ToMPos(map);
|
||||
if (regionMap[mpos] == NullRegion)
|
||||
{
|
||||
if (fullyPlayable && playable[mpos] == Playability.Playable)
|
||||
{
|
||||
AddToRegion(cpos, true);
|
||||
return true;
|
||||
}
|
||||
else if (playable[mpos] == Playability.Partial)
|
||||
{
|
||||
AddToRegion(cpos, false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
CellLayerUtils.FloodFill(
|
||||
playable,
|
||||
[(start, true)],
|
||||
Filler,
|
||||
DirectionExts.Spread4CVec);
|
||||
}
|
||||
|
||||
foreach (var mpos in map.AllCells.MapCoords)
|
||||
if (regionMap[mpos] == NullRegion && playable[mpos] == Playability.Playable)
|
||||
{
|
||||
var region = new Region()
|
||||
{
|
||||
Area = 0,
|
||||
PlayableArea = 0,
|
||||
Id = regions.Count,
|
||||
};
|
||||
|
||||
regions.Add(region);
|
||||
var cpos = mpos.ToCPos(map);
|
||||
Fill(region, cpos);
|
||||
}
|
||||
|
||||
return (regions.ToArray(), regionMap, playable);
|
||||
}
|
||||
}
|
||||
}
|
||||
1752
OpenRA.Mods.Common/MapGenerator/Terraformer.cs
Normal file
1752
OpenRA.Mods.Common/MapGenerator/Terraformer.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -241,6 +241,53 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
Brushes = permittedTemplates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience method to create TilingPaths using common settings.
|
||||
/// Start and end terminal types are the same.
|
||||
/// PermittedSegments are derived from brushes and inner/terminal types.
|
||||
/// Loops will automatically use only the inner type.
|
||||
/// Uses automatic end deviation and loop optimization.
|
||||
/// </summary>
|
||||
public static TilingPath QuickCreate(
|
||||
Map map,
|
||||
IReadOnlyList<MultiBrush> brushes,
|
||||
CPos[] points,
|
||||
int maxDeviation,
|
||||
string innerSegmentType,
|
||||
string terminalSegmentType)
|
||||
{
|
||||
var nonLoopedRoadPermittedTemplates =
|
||||
PermittedSegments.FromInnerAndTerminalTypes(
|
||||
brushes, [innerSegmentType], [terminalSegmentType]);
|
||||
var loopedRoadPermittedTemplates =
|
||||
PermittedSegments.FromType(brushes, [innerSegmentType]);
|
||||
|
||||
var isLoop = points[0] == points[^1];
|
||||
TilingPath path;
|
||||
if (isLoop)
|
||||
path = new TilingPath(
|
||||
map,
|
||||
points,
|
||||
maxDeviation,
|
||||
innerSegmentType,
|
||||
innerSegmentType,
|
||||
loopedRoadPermittedTemplates);
|
||||
else
|
||||
path = new TilingPath(
|
||||
map,
|
||||
points,
|
||||
maxDeviation,
|
||||
terminalSegmentType,
|
||||
terminalSegmentType,
|
||||
nonLoopedRoadPermittedTemplates);
|
||||
|
||||
path
|
||||
.SetAutoEndDeviation()
|
||||
.OptimizeLoop();
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
sealed class TilingSegment
|
||||
{
|
||||
public readonly MultiBrush MultiBrush;
|
||||
@@ -1360,6 +1407,46 @@ namespace OpenRA.Mods.Common.MapGenerator
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Applies StraightenEndsPathPoints to this TilingPath, returning this.</summary>
|
||||
public TilingPath StraightenEnds(
|
||||
int shrink,
|
||||
int grow,
|
||||
int minimumLength,
|
||||
int growthInertialRange)
|
||||
{
|
||||
Points = StraightenEndsPathPoints(
|
||||
Points,
|
||||
CellLayerUtils.CellBounds(Map),
|
||||
shrink,
|
||||
grow,
|
||||
minimumLength,
|
||||
growthInertialRange);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Straighten the start and end of a path by shrinking and regrowing a straight section.
|
||||
/// </summary>
|
||||
/// <param name="points">Points of the path.</param>
|
||||
/// <param name="bounds">Map bounds, used to identify paths touching edges.</param>
|
||||
/// <param name="shrink">Distance to shrink path ends (before regrowing them).</param>
|
||||
/// <param name="grow">Distance to regrow path ends with straightening (after shrinking).</param>
|
||||
/// <param name="minimumLength">The minimum length (after shrinking, before growth) that paths may be.</param>
|
||||
/// <param name="growthInertialRange">How many points are used to decide the regrowth direction.</param>
|
||||
public static CPos[] StraightenEndsPathPoints(
|
||||
CPos[] points,
|
||||
Rectangle bounds,
|
||||
int shrink,
|
||||
int grow,
|
||||
int minimumLength,
|
||||
int growthInertialRange)
|
||||
{
|
||||
points = ExtendEdgePathPoints(points, bounds, 2 * shrink + minimumLength);
|
||||
points = ShrinkPathPoints(points, shrink, minimumLength);
|
||||
points = InertiallyExtendPathPoints(points, grow, growthInertialRange);
|
||||
return points;
|
||||
}
|
||||
|
||||
/// <summary>Set MaxEndDeviation.</summary>
|
||||
public TilingPath SetMaxEndDeviation(int maxEndDeviation)
|
||||
{
|
||||
|
||||
@@ -9,11 +9,9 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Mods.Common.MapGenerator;
|
||||
using OpenRA.Mods.Common.Terrain;
|
||||
using OpenRA.Support;
|
||||
using OpenRA.Traits;
|
||||
|
||||
@@ -76,43 +74,21 @@ namespace OpenRA.Mods.Common.Traits
|
||||
var random = new MersenneTwister();
|
||||
var terrainInfo = modData.DefaultTerrainInfo[args.Tileset];
|
||||
|
||||
var map = new Map(modData, terrainInfo, args.Size);
|
||||
var maxTerrainHeight = map.Grid.MaximumTerrainHeight;
|
||||
var tl = new PPos(1, 1 + maxTerrainHeight);
|
||||
var br = new PPos(args.Size.Width - 1, args.Size.Height + maxTerrainHeight - 1);
|
||||
map.SetBounds(tl, br);
|
||||
|
||||
if (!Exts.TryParseUshortInvariant(args.Settings.NodeWithKey("Tile").Value.Value, out var tileType))
|
||||
throw new YamlException("Illegal tile type");
|
||||
|
||||
var tile = new TerrainTile(tileType, 0);
|
||||
if (!terrainInfo.TryGetTerrainInfo(tile, out var _))
|
||||
if (!terrainInfo.TryGetTerrainInfo(new TerrainTile(tileType, 0), out var _))
|
||||
throw new MapGenerationException("Illegal tile type");
|
||||
|
||||
// If the default terrain tile is part of a PickAny template, pick
|
||||
// a random tile index. Otherwise, just use the default tile.
|
||||
Func<TerrainTile> tilePicker;
|
||||
if (map.Rules.TerrainInfo is ITemplatedTerrainInfo templatedTerrainInfo &&
|
||||
templatedTerrainInfo.Templates.TryGetValue(tileType, out var template) &&
|
||||
template.PickAny)
|
||||
{
|
||||
tilePicker = () => new TerrainTile(tileType, (byte)random.Next(0, template.TilesCount));
|
||||
}
|
||||
else
|
||||
{
|
||||
tilePicker = () => tile;
|
||||
}
|
||||
var map = new Map(modData, terrainInfo, args.Size);
|
||||
var terraformer = new Terraformer(args, map, modData, [], Symmetry.Mirror.None, 1);
|
||||
|
||||
foreach (var cell in map.AllCells)
|
||||
{
|
||||
var mpos = cell.ToMPos(map);
|
||||
map.Tiles[mpos] = tilePicker();
|
||||
map.Resources[mpos] = new ResourceTile(0, 0);
|
||||
map.Height[mpos] = 0;
|
||||
}
|
||||
terraformer.InitMap();
|
||||
|
||||
map.PlayerDefinitions = new MapPlayers(map.Rules, 0).ToMiniYaml();
|
||||
map.ActorDefinitions = [];
|
||||
foreach (var mpos in map.AllCells.MapCoords)
|
||||
map.Tiles[mpos] = terraformer.PickTile(random, tileType);
|
||||
|
||||
terraformer.BakeMap();
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@
|
||||
MaximumAltitude: 8
|
||||
RoughnessRadius: 5
|
||||
Roughness: 500
|
||||
MinimumTerrainContourSpacing: 6
|
||||
MinimumTerrainContourSpacing: 5
|
||||
MinimumCliffLength: 10
|
||||
ForestClumpiness: 1
|
||||
DenyWalledAreas: True
|
||||
@@ -60,11 +60,8 @@
|
||||
splitblue: BlueTiberium
|
||||
ClearTerrain: Clear
|
||||
PlayableTerrain: Beach,BlueTiberium,Bridge,Clear,Road,Rough,Tiberium,Wall
|
||||
PartiallyPlayableTerrain: River,Tree,Water
|
||||
UnplayableTerrain: Rock
|
||||
DominantTerrain: River,Rock,Tree,Water
|
||||
ZoneableTerrain: Clear,Road
|
||||
PartiallyPlayableCategories: Beach,Road
|
||||
ClearSegmentTypes: Clear
|
||||
BeachSegmentTypes: Beach
|
||||
CliffSegmentTypes: Cliff
|
||||
@@ -158,16 +155,14 @@
|
||||
Label: label-cnc-map-generator-choice-terrain-type-mountains
|
||||
Settings:
|
||||
Water: 0
|
||||
Mountains: 1000
|
||||
Mountains: 900
|
||||
Roughness: 600
|
||||
MinimumTerrainContourSpacing: 5
|
||||
Choice@MountainLakes:
|
||||
Label: label-cnc-map-generator-choice-terrain-type-mountain-lakes
|
||||
Settings:
|
||||
Water: 200
|
||||
Mountains: 1000
|
||||
Mountains: 900
|
||||
Roughness: 850
|
||||
MinimumTerrainContourSpacing: 5
|
||||
MultiChoiceOption@Shape:
|
||||
Label: label-cnc-map-generator-option-shape
|
||||
Default: Square
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
MaximumAltitude: 8
|
||||
RoughnessRadius: 5
|
||||
Roughness: 500
|
||||
MinimumTerrainContourSpacing: 6
|
||||
MinimumTerrainContourSpacing: 5
|
||||
MinimumCliffLength: 10
|
||||
ForestClumpiness: 1
|
||||
DenyWalledAreas: True
|
||||
@@ -59,11 +59,8 @@
|
||||
gmine: Gems
|
||||
ClearTerrain: Clear
|
||||
PlayableTerrain: Beach,Bridge,Clear,Gems,Ore,Road,Rough,Wall,Water
|
||||
PartiallyPlayableTerrain: Tree
|
||||
UnplayableTerrain: River,Rock
|
||||
DominantTerrain: River,Rock,Tree,Water
|
||||
ZoneableTerrain: Clear,Road
|
||||
PartiallyPlayableCategories: Beach,Road
|
||||
ClearSegmentTypes: Clear
|
||||
BeachSegmentTypes: Beach
|
||||
CliffSegmentTypes: Cliff
|
||||
@@ -156,16 +153,14 @@
|
||||
Label: label-ra-map-generator-choice-terrain-type-mountains
|
||||
Settings:
|
||||
Water: 0
|
||||
Mountains: 1000
|
||||
Mountains: 900
|
||||
Roughness: 600
|
||||
MinimumTerrainContourSpacing: 5
|
||||
Choice@MountainLakes:
|
||||
Label: label-ra-map-generator-choice-terrain-type-mountain-lakes
|
||||
Settings:
|
||||
Water: 200
|
||||
Mountains: 1000
|
||||
Mountains: 900
|
||||
Roughness: 850
|
||||
MinimumTerrainContourSpacing: 5
|
||||
Choice@Oceanic:
|
||||
Label: label-ra-map-generator-choice-terrain-type-oceanic
|
||||
Settings:
|
||||
|
||||
Reference in New Issue
Block a user