diff --git a/OpenRA.Mods.Common/MapGenerator/ActorPlan.cs b/OpenRA.Mods.Common/MapGenerator/ActorPlan.cs
index 4d36c42bd9..1a12aa39aa 100644
--- a/OpenRA.Mods.Common/MapGenerator/ActorPlan.cs
+++ b/OpenRA.Mods.Common/MapGenerator/ActorPlan.cs
@@ -145,5 +145,16 @@ namespace OpenRA.Mods.Common.MapGenerator
return CellLayerUtils.CVecToWVec(new CVec(left + right, top + bottom), Map.Grid.Type) / 2;
}
+
+ /// Return the larger of the width or height of the actor's footprint.
+ 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;
+ }
}
}
diff --git a/OpenRA.Mods.Common/MapGenerator/CellLayerUtils.cs b/OpenRA.Mods.Common/MapGenerator/CellLayerUtils.cs
index 746641e70a..8f9b74c287 100644
--- a/OpenRA.Mods.Common/MapGenerator/CellLayerUtils.cs
+++ b/OpenRA.Mods.Common/MapGenerator/CellLayerUtils.cs
@@ -60,6 +60,25 @@ namespace OpenRA.Mods.Common.MapGenerator
}
}
+ 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)
{
@@ -218,7 +237,7 @@ namespace OpenRA.Mods.Common.MapGenerator
public static void OverCircle(
CellLayer cellLayer,
WPos wCenter,
- int wRadius,
+ WDist wRadius,
bool outside,
Action 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
}
///
- /// 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.
///
public static void CalibrateQuantileInPlace(CellLayer cellLayer, int target, int count, int outOf)
{
@@ -300,6 +321,32 @@ namespace OpenRA.Mods.Common.MapGenerator
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.
///
@@ -419,10 +466,10 @@ namespace OpenRA.Mods.Common.MapGenerator
/// Returns world distances (1024ths).
///
public static void WalkingDistances(
- CellLayer distances,
+ CellLayer distances,
CellLayer passable,
IEnumerable seeds,
- int maxDistance)
+ WDist maxDistance)
{
var passableMatrix = ToMatrix(passable, false);
var cellBounds = CellBounds(passable);
@@ -532,5 +579,111 @@ namespace OpenRA.Mods.Common.MapGenerator
}
}
}
+
+ ///
+ /// 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;
+ }
}
}
diff --git a/OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs b/OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs
index e7fb300daa..2d686002f5 100644
--- a/OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs
+++ b/OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs
@@ -23,6 +23,164 @@ namespace OpenRA.Mods.Common.MapGenerator
{
public const int MaxBinomialKernelRadius = 10;
+ public enum DumpAdjustment
+ {
+ /// Make no adjustment.
+ None,
+
+ /// Normalize the matrix amplitude to the color range.
+ Normalize,
+
+ ///
+ /// Normalize the matrix amplitude, but uniformally extend away from zero by a small
+ /// amount to help identify the sign of martix values.
+ ///
+ Emphasize,
+ }
+
+ public enum GraphMode
+ {
+ ///
+ /// The plotted value is the latest sequence touching a cell + 1.
+ ///
+ Identifier,
+
+ ///
+ /// The plotted value is the (latest) point index in the (latest) sequence touching a
+ /// cell.
+ ///
+ Gradient,
+
+ /// The plotted value is the count of points touching a cell.
+ Accumulate,
+ }
+
+ ///
+ ///
+ /// Debugging method that prints a matrix to stderr using color only (not value listing).
+ ///
+ ///
+ /// Orange < -255, -255 <= Red < 0, Black == 0, 0 < Blue <= 255,
+ /// 255 < Cyan. Faint green is used for distance markings.
+ ///
+ ///
+ /// The matrix can optionally be preprocessed for easier visual interpretation using a
+ /// DumpAdjustment.
+ ///
+ ///
+ public static void ColorDump2d(
+ string label,
+ Matrix 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 matrix)
+ {
+ ColorDump2d(label, matrix.Map(v => v ? 255 : -255));
+ }
+
+ ///
+ /// 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.
+ ///
+ public static void EnumDump2d(string label, Matrix 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(string label, Matrix matrix) where T : Enum
+ {
+ EnumDump2d(label, matrix.Map(v => Convert.ToInt32(v, NumberFormatInfo.InvariantInfo)));
+ }
+
///
/// Debugging method that prints a matrix to stderr.
///
@@ -96,6 +254,55 @@ namespace OpenRA.Mods.Common.MapGenerator
Console.Error.Flush();
}
+ ///
+ /// Plot multiple point sequences onto a matrix for debugging visualization. The matrix is
+ /// fit to the shape of all the path.
+ ///
+ public static Matrix GraphPoints(
+ IEnumerable> 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(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(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;
+ }
+
+ ///
+ /// Plot a point sequence onto a matrix for debugging visualization. The matrix is fit to
+ /// the shape of the path.
+ ///
+ public static Matrix GraphPoints(
+ IEnumerable points,
+ GraphMode mode = GraphMode.Identifier)
+ {
+ return GraphPoints([points], mode);
+ }
+
///
///
/// Perform a generic flood fill starting at seeds [(xy, prop), ...].
@@ -166,12 +373,12 @@ namespace OpenRA.Mods.Common.MapGenerator
/// int.MaxValue.
///
///
- public static Matrix WalkingDistances(Matrix passable, IEnumerable seeds, int maxDistance)
+ public static Matrix WalkingDistances(Matrix passable, IEnumerable seeds, WDist maxDistance)
{
const int Diagonal = 1448;
const int Straight = 1024;
- var output = new Matrix(passable.Size).Fill(int.MaxValue);
+ var output = new Matrix(passable.Size).Fill(WDist.MaxValue);
var unprocessed = new PriorityArray(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
}
///
- /// 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.
///
public static void CalibrateQuantileInPlace(Matrix matrix, int target, int count, int outOf)
{
@@ -724,6 +933,23 @@ namespace OpenRA.Mods.Common.MapGenerator
matrix[i] += adjustment;
}
+ ///
+ /// 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.
+ ///
+ public static Matrix CalibratedBooleanThreshold(Matrix input, int count, int outOf)
+ {
+ if (count <= 0)
+ return new Matrix(input.Size);
+ else if (count >= outOf)
+ return new Matrix(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);
+ }
+
///
/// 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
///
/// 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).
///
///
/// 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.
///
+ ///
+ /// If no points are on or close enough to the matrix area, returns null.
+ ///
///
public static Matrix PointsChirality(int2 size, IEnumerable 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)
{
diff --git a/OpenRA.Mods.Common/MapGenerator/MultiBrush.cs b/OpenRA.Mods.Common/MapGenerator/MultiBrush.cs
index 574db23aeb..806a9e21f6 100644
--- a/OpenRA.Mods.Common/MapGenerator/MultiBrush.cs
+++ b/OpenRA.Mods.Common/MapGenerator/MultiBrush.cs
@@ -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;
}
diff --git a/OpenRA.Mods.Common/MapGenerator/NoiseUtils.cs b/OpenRA.Mods.Common/MapGenerator/NoiseUtils.cs
index 651e16b9b1..b0b02edcf2 100644
--- a/OpenRA.Mods.Common/MapGenerator/NoiseUtils.cs
+++ b/OpenRA.Mods.Common/MapGenerator/NoiseUtils.cs
@@ -20,9 +20,24 @@ namespace OpenRA.Mods.Common.MapGenerator
const int Scale = 1024;
const int ScaledSqrt2 = 1448;
+ /// Amplitude is the same for all wavelengths.
+ public static int WhiteAmplitude(int wavelength) => 1;
+
/// Amplitude proportional to wavelength.
public static int PinkAmplitude(int wavelength) => wavelength;
+ ///
+ /// amplitude = wavelength ** (1 / (2 ** clumpiness))
+ /// Setting clumpiness to 0 is equivalent to pink noise.
+ ///
+ public static int ClumpinessAmplitude(int wavelength, int clumpiness)
+ {
+ var amplitude = wavelength;
+ for (var i = 0; i < clumpiness; i++)
+ amplitude = Exts.ISqrt(amplitude);
+ return amplitude;
+ }
+
///
///
/// Create noise by combining multiple layers of Perlin noise of halving wavelengths.
diff --git a/OpenRA.Mods.Common/MapGenerator/PlayableSpace.cs b/OpenRA.Mods.Common/MapGenerator/PlayableSpace.cs
deleted file mode 100644
index 0f31fd1391..0000000000
--- a/OpenRA.Mods.Common/MapGenerator/PlayableSpace.cs
+++ /dev/null
@@ -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
- {
- /// Area is unplayable by land/naval units.
- Unplayable = 0,
-
- ///
- /// 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.
- ///
- Partial = 1,
-
- ///
- /// Area is playable by either land or naval units.
- ///
- Playable = 2,
- }
-
- ///
- /// Additional data for a region containing playable space.
- /// The shape of a region is specified separately via a region mask.
- ///
- public sealed class Region
- {
- /// Area of playable and partially playable space.
- public int Area;
-
- /// Area of fully playable space.
- public int PlayableArea;
-
- /// Region ID.
- public int Id;
- }
-
- /// Sentinel indicating a position isn't assigned to a region.
- public const int NullRegion = -1;
-
- ///
- ///
- /// Analyses a given map's tiles and ActorPlans and determines the playable space within it.
- ///
- ///
- /// Requires a playabilityMap which specifies whether certain tiles are considered playable
- /// or not. Actors are always considered partially playable.
- ///
- ///
- /// RegionMap contains the mapping of map positions to Regions. If a map position is not
- /// within a region, the value is NullRegion.
- ///
- ///
- public static (Region[] Regions, CellLayer RegionMap, CellLayer Playable) FindPlayableRegions(
- Map map,
- List actorPlans,
- Dictionary playabilityMap)
- {
- var regions = new List();
- var regionMap = new CellLayer(map);
- regionMap.Clear(NullRegion);
- var playable = new CellLayer(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);
- }
- }
-}
diff --git a/OpenRA.Mods.Common/MapGenerator/Terraformer.cs b/OpenRA.Mods.Common/MapGenerator/Terraformer.cs
new file mode 100644
index 0000000000..5c0ea80908
--- /dev/null
+++ b/OpenRA.Mods.Common/MapGenerator/Terraformer.cs
@@ -0,0 +1,1752 @@
+#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.Mods.Common.Terrain;
+using OpenRA.Mods.Common.Traits;
+using OpenRA.Primitives;
+using OpenRA.Support;
+using static OpenRA.Mods.Common.Traits.ResourceLayerInfo;
+
+namespace OpenRA.Mods.Common.MapGenerator
+{
+ /// Collection of high-level map generation utilities.
+ public class Terraformer
+ {
+ /// Common denominator for fractional arguments.
+ public const int FractionMax = 1000;
+
+ /// Biases or excludes resources at a location during resource planning.
+ public sealed class ResourceBias
+ {
+ /// The location of the bias.
+ public WPos WPos;
+
+ /// Resources will not be placed within this distance of the actor.
+ public WDist? ExclusionRadius = null;
+
+ /// Resources will be biased within this radius.
+ public WDist? BiasRadius = null;
+
+ ///
+ /// Biasing function, applied either to all resources or the specific ResourceType.
+ /// Maps the original value and the squared-WDist-from-CPos to a new value.
+ ///
+ public Func Bias = null;
+
+ /// If non-null, encourages resources to become this type.
+ public ResourceTypeInfo ResourceType = null;
+
+ /// Create a bias at a location.
+ public ResourceBias(WPos wpos)
+ {
+ WPos = wpos;
+ }
+
+ /// Create a bias at an actor's location.
+ public ResourceBias(ActorPlan actorPlan)
+ : this(actorPlan.WPosCenterLocation)
+ { }
+ }
+
+ ///
+ /// Metadata for the values in a CellLayer matching an ID.
+ ///
+ public sealed class Region
+ {
+ public const int NullId = -1;
+
+ /// Region ID.
+ public int Id;
+
+ /// Area of the region.
+ public int Area;
+ }
+
+ public enum Side : sbyte
+ {
+ Out = -1,
+ None = 0,
+ In = 1,
+ }
+
+ public static (T[] Types, U[] Weights) SplitDictionary(IReadOnlyDictionary typeWeights)
+ {
+ var types = typeWeights
+ .Select(kv => kv.Key)
+ .Order()
+ .ToArray();
+ var weights = types
+ .Select(type => typeWeights[type])
+ .ToArray();
+ return (types, weights);
+ }
+
+ public readonly MapGenerationArgs MapGenerationArgs;
+ public readonly Map Map;
+ public readonly ModData ModData;
+ public readonly List ActorPlans;
+ public readonly Symmetry.Mirror Mirror;
+ public readonly int Rotations;
+
+ readonly ITerrainInfo terrainInfo;
+
+ // Will be null if terrainInfo isn't a ITemplatedTerrainInfo. Some methods assume that the
+ // terrainInfo is an ITemplatedTerrainInfo.
+ readonly ITemplatedTerrainInfo templatedTerrainInfo;
+ readonly Lazy> lazyProjectionSpacing;
+
+ public Terraformer(
+ MapGenerationArgs mapGenerationArgs,
+ Map map,
+ ModData modData,
+ List actorPlans,
+ Symmetry.Mirror mirror,
+ int rotations)
+ {
+ MapGenerationArgs = mapGenerationArgs;
+ Map = map;
+ ModData = modData;
+ ActorPlans = actorPlans;
+ Mirror = mirror;
+ Rotations = rotations;
+
+ terrainInfo = modData.DefaultTerrainInfo[map.Tileset];
+ templatedTerrainInfo = terrainInfo as ITemplatedTerrainInfo;
+
+ lazyProjectionSpacing = new(ProjectionSpacing);
+ }
+
+ public void CheckHasMapShapeOrNull(CellLayer layer)
+ {
+ if (layer != null)
+ CheckHasMapShape(layer);
+ }
+
+ public void CheckHasMapShapeOrNull(Matrix layer)
+ {
+ if (layer != null)
+ CheckHasMapShape(layer);
+ }
+
+ public void CheckHasMapShape(CellLayer layer)
+ {
+ if (!CellLayerUtils.AreSameShape(layer, Map.Tiles))
+ throw new ArgumentException("CellLayer has different shape to map");
+ }
+
+ public void CheckHasMapShape(Matrix matrix)
+ {
+ var cellBounds = CellLayerUtils.CellBounds(Map);
+ var size = cellBounds.Size.ToInt2();
+ if (matrix.Size != size)
+ throw new ArgumentException("Matrix has different shape to map");
+ }
+
+ ///
+ /// Enumerates through all current ActorPlans of the given type.
+ ///
+ public IEnumerable ActorsOfType(string type)
+ {
+ return ActorPlans.Where(a => a.Reference.Type == type);
+ }
+
+ /// Perform some basic initialization of a map.
+ public void InitMap()
+ {
+ var maxTerrainHeight = Map.Grid.MaximumTerrainHeight;
+ var tl = new PPos(1, 1 + maxTerrainHeight);
+ var br = new PPos(Map.MapSize.Width - 2, Map.MapSize.Height + maxTerrainHeight - 2);
+ Map.SetBounds(tl, br);
+ Map.Title = MapGenerationArgs.Title;
+ Map.Author = MapGenerationArgs.Author;
+ Map.RequiresMod = ModData.Manifest.Id;
+ }
+
+ ///
+ /// Commits draft data to the map, such as player and actor definitions.
+ ///
+ public void BakeMap()
+ {
+ var playerCount = ActorsOfType("mpspawn").Count();
+ Map.PlayerDefinitions = new MapPlayers(Map.Rules, playerCount).ToMiniYaml();
+ Map.ActorDefinitions = ActorPlans
+ .Select((plan, i) => new MiniYamlNode($"Actor{i}", plan.Reference.Save()))
+ .ToImmutableArray();
+ }
+
+ ///
+ /// Return a new CellLayer produced by aggregating projected cells from an input CellLayer.
+ /// The input does not need to have the same shape as the map.
+ ///
+ public CellLayer ImproveSymmetry(
+ CellLayer layer,
+ T outsideValue,
+ Func aggregator)
+ {
+ var newLayer = new CellLayer(layer.GridType, layer.Size);
+ Symmetry.RotateAndMirrorOverCPos(
+ layer,
+ Rotations,
+ Mirror,
+ (sources, destination)
+ => newLayer[destination] = sources
+ .Select(source => layer.TryGetValue(source, out var value) ? value : outsideValue)
+ .Aggregate(aggregator));
+ return newLayer;
+ }
+
+ ///
+ /// Subtract an actor's footprint from zoneable. Optionally, a circle with a given dezone
+ /// radius from the actor center can also be subtracted from zoneable.
+ ///
+ public void DezoneActor(
+ ActorPlan actorPlan,
+ CellLayer zoneable,
+ WDist? dezoneRadius = null)
+ {
+ CheckHasMapShape(zoneable);
+
+ foreach (var (cpos, _) in actorPlan.Footprint())
+ if (zoneable.Contains(cpos))
+ zoneable[cpos] = false;
+
+ if (dezoneRadius.HasValue)
+ {
+ CellLayerUtils.OverCircle(
+ cellLayer: zoneable,
+ wCenter: actorPlan.WPosCenterLocation,
+ wRadius: dezoneRadius.Value,
+ outside: false,
+ action: (mpos, _, _, _) => zoneable[mpos] = false);
+ }
+ }
+
+ /// Sets all zoneable cells where the map has actor footprints to false.
+ public void ZoneFromActors(CellLayer zoneable, T value)
+ {
+ foreach (var actorPlan in ActorPlans)
+ foreach (var (cpos, _) in actorPlan.Footprint())
+ if (zoneable.Contains(cpos))
+ zoneable[cpos] = value;
+ }
+
+ /// Sets all zoneable cells where the map has resources to false.
+ public void ZoneFromResources(CellLayer zoneable, T value)
+ {
+ CheckHasMapShape(zoneable);
+
+ foreach (var mpos in Map.AllCells.MapCoords)
+ if (Map.Resources[mpos].Type != 0)
+ zoneable[mpos] = value;
+ }
+
+ public void ZoneFromOutOfBounds(CellLayer zoneable, T value)
+ {
+ foreach (var mpos in Map.AllCells.MapCoords)
+ if (!Map.Contains(mpos))
+ zoneable[mpos] = value;
+ }
+
+ ///
+ /// Returns a CellLayer describing whether the space in a map satisfies given terrain types
+ /// (if allowedTerrain is non-null), is free of actors, and/or is free of resources.
+ ///
+ public CellLayer CheckSpace(
+ IReadOnlySet allowedTerrain,
+ bool checkActors = false,
+ bool checkResources = false,
+ bool checkBounds = false)
+ {
+ var space = new CellLayer(Map);
+ if (allowedTerrain != null)
+ {
+ foreach (var mpos in Map.AllCells.MapCoords)
+ space[mpos] = allowedTerrain.Contains(terrainInfo.GetTerrainIndex(Map.Tiles[mpos]));
+ }
+ else
+ {
+ space.Clear(true);
+ }
+
+ if (checkActors)
+ ZoneFromActors(space, false);
+
+ if (checkResources)
+ ZoneFromResources(space, false);
+
+ if (checkBounds)
+ ZoneFromOutOfBounds(space, false);
+
+ return space;
+ }
+
+ ///
+ /// Returns a CellLayer describing whether the space in a map has the given tile type and
+ /// is free of actors and/or resources.
+ ///
+ public CellLayer CheckSpace(
+ ushort requiredTile,
+ bool checkActors = false,
+ bool checkResources = false,
+ bool checkBounds = false)
+ {
+ var space = new CellLayer(Map);
+ foreach (var mpos in Map.AllCells.MapCoords)
+ space[mpos] = Map.Tiles[mpos].Type == requiredTile;
+
+ if (checkActors)
+ ZoneFromActors(space, false);
+
+ if (checkResources)
+ ZoneFromResources(space, false);
+
+ if (checkBounds)
+ ZoneFromOutOfBounds(space, false);
+
+ return space;
+ }
+
+ ///
+ /// Shrink zoneable areas by a given thickness in cells. Zones will be shrunk even if they
+ /// border the edge of the map.
+ ///
+ public CellLayer ErodeZones(CellLayer zoneable, int amount)
+ {
+ CheckHasMapShape(zoneable);
+ var roominess = new CellLayer(Map);
+ CellLayerUtils.ChebyshevRoom(roominess, zoneable, false);
+ return CellLayerUtils.Map(roominess, r => r > amount);
+ }
+
+ ///
+ /// Derives a CellLayer identifying the space in a map available for various actors,
+ /// resources, decorations, etc. A mask (usually playable space) can be used to further
+ /// limit the zoneable area.
+ ///
+ public CellLayer GetZoneable(
+ IReadOnlySet zoneableTerrain,
+ CellLayer mask = null)
+ {
+ CheckHasMapShapeOrNull(mask);
+
+ var zoneable = CheckSpace(zoneableTerrain, true, true, true);
+ if (mask != null)
+ zoneable = CellLayerUtils.Intersect([zoneable, mask]);
+
+ if (Rotations > 1 || Mirror != Symmetry.Mirror.None)
+ {
+ // Reserve the center of the map - otherwise it will mess with symmetries
+ CellLayerUtils.OverCircle(
+ cellLayer: zoneable,
+ wCenter: CellLayerUtils.Center(Map),
+ wRadius: new WDist(1024),
+ outside: false,
+ action: (mpos, _, _, _) => zoneable[mpos] = false);
+ }
+
+ zoneable = ImproveSymmetry(zoneable, false, (a, b) => a && b);
+
+ return zoneable;
+ }
+
+ /// Create map-shaped CellLayer preinitialized with a circle.
+ public CellLayer CenteredCircle(T inside, T outside, WDist radius)
+ {
+ var circle = new CellLayer(Map);
+ circle.Clear(outside);
+ CellLayerUtils.OverCircle(
+ cellLayer: circle,
+ wCenter: CellLayerUtils.Center(Map),
+ wRadius: radius,
+ outside: false,
+ action: (mpos, _, _, _) => circle[mpos] = inside);
+ return circle;
+ }
+
+ ///
+ /// Return a CellLayer where each cell is half the minimum distances to one of its symmetry
+ /// projections. Can be used to avoid placing actors too close to their own projections.
+ ///
+ public CellLayer ProjectionSpacing()
+ {
+ var projectionSpacing = new CellLayer(Map);
+ Symmetry.RotateAndMirrorOverCPos(
+ projectionSpacing,
+ Rotations,
+ Mirror,
+ (projections, cpos) =>
+ projectionSpacing[cpos] = Symmetry.ProjectionProximity(projections) / 2);
+ return projectionSpacing;
+ }
+
+ ///
+ /// Produce a cell layer which identifies assymetries in the map.
+ /// Cells that are considered recessive but that have dominant projections are marked as
+ /// true in the resulting CellLayer.
+ ///
+ /// Cells matching these terrain types are consided dominant.
+ /// If true, cells covered by actors are considered dominant.
+ ///
+ /// Also mark as true any cells where the terrain types don't match with projections, even
+ /// if they are also all recessive.
+ ///
+ public CellLayer FindAsymmetries(
+ IReadOnlySet dominantTerrain,
+ bool dominantActors,
+ bool strictTerrainTypes)
+ {
+ var terrainTypes = CellLayerUtils.Create(Map, (MPos mpos) =>
+ terrainInfo.GetTerrainIndex(Map.Tiles[mpos]));
+ var dominant = CellLayerUtils.Map(terrainTypes, dominantTerrain.Contains);
+ if (dominantActors)
+ ZoneFromActors(dominant, true);
+
+ var incompatibilities = new CellLayer(Map);
+ Symmetry.RotateAndMirrorOverCPos(
+ incompatibilities,
+ Rotations,
+ Mirror,
+ (CPos[] sources, CPos destination) =>
+ {
+ if (!dominant[destination])
+ incompatibilities[destination] = sources
+ .Where(incompatibilities.Contains)
+ .Any(source => dominant[source] || (strictTerrainTypes && terrainTypes[destination] != terrainTypes[source]));
+ });
+ return incompatibilities;
+ }
+
+ ///
+ /// Given a space CellLayer, identifies the separate true regions. Cells are part of the
+ /// same region if they are connected by an offset in spread.
+ ///
+ public (Region[] Regions, CellLayer RegionMap) FindRegions(
+ CellLayer space,
+ ImmutableArray spread)
+ {
+ CheckHasMapShape(space);
+
+ var regions = new List();
+ var regionMap = new CellLayer(Map);
+ regionMap.Clear(Region.NullId);
+
+ void Fill(Region region, CPos start)
+ {
+ bool? Filler(CPos cpos, bool _)
+ {
+ var mpos = cpos.ToMPos(Map);
+ if (regionMap[mpos] == Region.NullId && space[mpos])
+ {
+ regionMap[mpos] = region.Id;
+ region.Area++;
+ return true;
+ }
+
+ return null;
+ }
+
+ CellLayerUtils.FloodFill(
+ space,
+ [(start, true)],
+ Filler,
+ spread);
+ }
+
+ foreach (var mpos in Map.AllCells.MapCoords)
+ if (regionMap[mpos] == Region.NullId && space[mpos])
+ {
+ var region = new Region()
+ {
+ Id = regions.Count,
+ Area = 0,
+ };
+
+ regions.Add(region);
+ var cpos = mpos.ToCPos(Map);
+ Fill(region, cpos);
+ }
+
+ return (regions.ToArray(), regionMap);
+ }
+
+ ///
+ /// Finds the largest, symmetrical, unpoisoned playable region on the map.
+ /// Returns a CellLayer describing the playable region, or null if there is no suitable
+ /// playable region.
+ ///
+ /// Whether given cells are playable.
+ /// Any regions with a poisoned cell are disqualified. Can be null.
+ public CellLayer ChoosePlayableRegion(
+ CellLayer playable,
+ CellLayer poison = null)
+ {
+ CheckHasMapShapeOrNull(poison);
+
+ var (regions, regionMask) = FindRegions(playable, DirectionExts.Spread8CVec);
+ var disqualifications = new HashSet();
+
+ if (poison != null)
+ foreach (var mpos in Map.AllCells.MapCoords)
+ if (poison[mpos]
+ && regionMask[mpos] != Region.NullId
+ && playable[mpos])
+ disqualifications.Add(regionMask[mpos]);
+
+ // Disqualify regions that violate any symmetry requirements.
+ {
+ var symmetryScore = new int[regions.Length];
+ void TestSymmetry(CPos[] sources, CPos destination)
+ {
+ var id = regionMask[destination];
+ if (!playable[destination])
+ return;
+ if (sources.All(source => regionMask.TryGetValue(source, out var sourceId) && sourceId == id))
+ symmetryScore[id]++;
+ }
+
+ Symmetry.RotateAndMirrorOverCPos(
+ regionMask,
+ Rotations,
+ Mirror,
+ TestSymmetry);
+
+ for (var id = 0; id < symmetryScore.Length; id++)
+ if (symmetryScore[id] < regions[id].Area / 2)
+ disqualifications.Add(id);
+ }
+
+ Region largest = null;
+ foreach (var region in regions)
+ {
+ if (disqualifications.Contains(region.Id))
+ continue;
+ if (largest == null || region.Area > largest.Area)
+ largest = region;
+ }
+
+ if (largest == null)
+ return null;
+
+ return CellLayerUtils.Create(Map, (MPos mpos) => regionMask[mpos] == largest.Id);
+ }
+
+ ///
+ /// Generate a CellLayer containing scores for the preferability of spawn locations, based
+ /// on separation from symmetry projections and the map center. Higher scores are better.
+ ///
+ ///
+ /// Distance from the map center or symmetry lines inside of which spawns are biased away
+ /// from. Measured as a fraction (out of 1024) of the map's smallest dimension.
+ ///
+ public CellLayer SpawnBias(int centralReservationFraction)
+ {
+ var minSpan = Math.Min(Map.MapSize.Width, Map.MapSize.Height);
+ var projectionSpacing = lazyProjectionSpacing.Value;
+ var spawnBias = new CellLayer(Map);
+ var spawnBiasRadius = Math.Max(1, minSpan * centralReservationFraction / FractionMax);
+ spawnBias.Clear(spawnBiasRadius);
+ CellLayerUtils.OverCircle(
+ cellLayer: spawnBias,
+ wCenter: CellLayerUtils.Center(Map),
+ wRadius: new WDist(1024 * spawnBiasRadius),
+ outside: false,
+ action: (mpos, _, _, wrSq) => spawnBias[mpos] = (int)Exts.ISqrt(wrSq) / 1024);
+ foreach (var mpos in Map.AllCells.MapCoords)
+ spawnBias[mpos] = Math.Min(spawnBias[mpos], projectionSpacing[mpos]);
+ return spawnBias;
+ }
+
+ ///
+ /// Finds a random suitable mpspawn location, biased away from symmetries and the map
+ /// center. Returns null if nowhere is suitable.
+ ///
+ /// Random source for spawn placement.
+ /// Mask of valid space for spawn (and other object) placement.
+ ///
+ /// Distance from the map center or symmetry lines inside of which spawns are biased away
+ /// from. Measured as a fraction (out of 1024) of the map's smallest dimension.
+ ///
+ /// Minimum space required for a spawn.
+ /// Maximum space used by a spawn, beyond which larger spaces are equally preferable.
+ ///
+ /// Space that spawns are expected to reserve in zoneable. Note that this function does not
+ /// modify zoneable, but this is needed in order to avoid placing symmetry-projected spawns
+ /// with overlapping zone allocations.
+ ///
+ public CPos? ChooseSpawnInZoneable(
+ MersenneTwister random,
+ CellLayer zoneable,
+ int centralReservationFraction,
+ int minimumRadius,
+ int maximumRadius,
+ int zoneRadius)
+ {
+ CheckHasMapShape(zoneable);
+ var projectionSpacing = lazyProjectionSpacing.Value;
+ var spawnBias = SpawnBias(centralReservationFraction);
+ var spawnPreference = new CellLayer(Map);
+ CellLayerUtils.ChebyshevRoom(spawnPreference, zoneable, false);
+ foreach (var mpos in Map.AllCells.MapCoords)
+ if (spawnPreference[mpos] >= minimumRadius &&
+ projectionSpacing[mpos] * 2 >= zoneRadius + minimumRadius)
+ {
+ spawnPreference[mpos] = spawnBias[mpos] * Math.Min(maximumRadius, spawnPreference[mpos]);
+ }
+ else
+ {
+ spawnPreference[mpos] = 0;
+ }
+
+ var (chosenMPos, chosenValue) = CellLayerUtils.FindRandomBest(
+ spawnPreference,
+ random,
+ (a, b) => a.CompareTo(b));
+
+ if (chosenValue < 1)
+ return null;
+
+ return chosenMPos.ToCPos(Map.Grid.Type);
+ }
+
+ ///
+ /// Find a random cell in zoneable with the most free space. Spaces which are maximumSpace
+ /// or more away from unzoned cells are treated equally.
+ /// Returns the CPos and space (up to maximumSpace) of the chosen cell.
+ /// The space value will be negative if there are no zoned cells.
+ ///
+ public (CPos CPos, int Space) ChooseInZoneable(
+ MersenneTwister random,
+ CellLayer zoneable,
+ int maximumSpace)
+ {
+ CheckHasMapShape(zoneable);
+ var projectionSpacing = lazyProjectionSpacing.Value;
+ var roominess = new CellLayer(Map);
+ CellLayerUtils.ChebyshevRoom(roominess, zoneable, false);
+ foreach (var mpos in Map.AllCells.MapCoords)
+ roominess[mpos] = Math.Min(
+ maximumSpace,
+ Math.Min(roominess[mpos], projectionSpacing[mpos]));
+ var (chosenMPos, chosenValue) = CellLayerUtils.FindRandomBest(
+ roominess,
+ random,
+ (a, b) => a.CompareTo(b));
+ return (chosenMPos.ToCPos(Map.Grid.Type), chosenValue);
+ }
+
+ ///
+ /// Generate a CellLayer scoring cells on how close to a target walking distance through
+ /// walkable cells they are from the closest seed point. Higher scores are better. The
+ /// score considers the distance needed to walk around unwalkable cells. Unsuitable cells
+ /// will have a score of -int.MaxValue.
+ ///
+ /// Walkable cells.
+ /// Unmasked cells will have a score of -int.MaxValue. Can be null.
+ /// Points from which to measure walking distance.
+ /// The highest scoring walking distance..
+ /// Distances greater than this are given a score of -int.MaxValue.
+ public CellLayer TargetWalkingDistance(
+ CellLayer walkable,
+ CellLayer mask,
+ IEnumerable seeds,
+ WDist targetRange,
+ WDist maximumRange)
+ {
+ CheckHasMapShape(walkable);
+ CheckHasMapShapeOrNull(mask);
+
+ var walkingDistances = new CellLayer(Map);
+ CellLayerUtils.WalkingDistances(
+ walkingDistances,
+ walkable,
+ seeds,
+ maximumRange);
+ var scores = new CellLayer(Map);
+ foreach (var mpos in Map.AllCells.MapCoords)
+ {
+ var v = (mask?[mpos] ?? true) ? walkingDistances[mpos].Length : int.MaxValue;
+ if (v == int.MaxValue)
+ scores[mpos] = -int.MaxValue;
+ else if (v <= targetRange.Length)
+ scores[mpos] = (v + 1023) / 1024;
+ else
+ scores[mpos] = (2 * targetRange.Length - v + 1023) / 1024;
+ }
+
+ return scores;
+ }
+
+ ///
+ /// Add an actor and its symmetry projections to the map and subtract its footprint from
+ /// zoneable. Optionally, a circle with a given dezone radius from the actor center can
+ /// also be subtracted from zoneable.
+ ///
+ public void ProjectPlaceDezoneActor(
+ ActorPlan actorPlan,
+ CellLayer zoneable = null,
+ WDist? dezoneRadius = null)
+ {
+ CheckHasMapShapeOrNull(zoneable);
+ var projections = Symmetry.RotateAndMirrorActorPlan(
+ actorPlan, Rotations, Mirror);
+ ActorPlans.AddRange(projections);
+ if (zoneable != null)
+ foreach (var projection in projections)
+ DezoneActor(projection, zoneable, dezoneRadius);
+ }
+
+ ///
+ /// Chooses a location for an actor within zoneable, and then projects, places, and dezones
+ /// for it. (The zoneable CellLayer is modified.)
+ ///
+ /// True if an actor was placed, false if there was insufficient space.
+ public bool AddActor(
+ MersenneTwister random,
+ CellLayer zoneable,
+ string actorType,
+ WDist? actorDezoneRadius = null)
+ {
+ var actorPlan = new ActorPlan(Map, actorType);
+
+ var requiredSpace = actorPlan.MaxSpan() * 1024 / 1448 + 2;
+ var (chosenCPos, chosenValue) = ChooseInZoneable(
+ random, zoneable, requiredSpace);
+ if (chosenValue < requiredSpace)
+ return false;
+
+ actorPlan.WPosCenterLocation = CellLayerUtils.CPosToWPos(chosenCPos, Map.Grid.Type);
+
+ ProjectPlaceDezoneActor(actorPlan, zoneable, actorDezoneRadius);
+
+ return true;
+ }
+
+ ///
+ /// Given a CellLayer of weights/priorities, chooses locations for actors within zoneable,
+ /// and then projects, places, and dezones for them.
+ ///
+ /// Random source for locations and actor type selection.
+ /// Available space for actors. Modified if actors placed.
+ /// Weights or priorities for placing an actor centered on cells.
+ /// Actor types to choose from and their relative weights.
+ /// Number of actors to attempt to place.
+ /// If true, choose actor locations using probabilistic weights instead of best candidate.
+ ///
+ /// Dezone radius for placed actors (in addition to footprint).
+ /// This does not affect spacing within the region.
+ ///
+ /// Number of actors added. 0 indicates none could be added.
+ public int AddDistributedActors(
+ MersenneTwister random,
+ CellLayer zoneable,
+ CellLayer distribution,
+ IReadOnlyDictionary weightedActorTypes,
+ int targetCount,
+ bool weighted,
+ WDist? actorDezoneRadius = null)
+ {
+ CheckHasMapShape(zoneable);
+ CheckHasMapShape(distribution);
+
+ var (actorTypes, actorTypeWeights) = SplitDictionary(weightedActorTypes);
+ var clusterZoneable = CellLayerUtils.Clone(zoneable);
+ for (var count = 0; count < targetCount; count++)
+ {
+ var actorType = actorTypes[random.PickWeighted(actorTypeWeights)];
+ var actorPlan = new ActorPlan(Map, actorType);
+ var requiredSpace = actorPlan.MaxSpan() * 1024 / 1448 + 2;
+
+ var roominess = new CellLayer(Map);
+ CellLayerUtils.ChebyshevRoom(roominess, clusterZoneable, false);
+ var filteredDistribution = CellLayerUtils.Create(Map, (MPos mpos) =>
+ roominess[mpos] >= requiredSpace ? distribution[mpos] : 0);
+
+ MPos mpos;
+ if (weighted)
+ mpos = CellLayerUtils.PickWeighted(filteredDistribution, random);
+ else
+ (mpos, _) = CellLayerUtils.FindRandomBest(filteredDistribution, random, (a, b) => a.CompareTo(b));
+
+ if (filteredDistribution[mpos] == 0)
+ return count;
+
+ actorPlan.Location = mpos.ToCPos(Map.Grid.Type);
+ CellLayerUtils.OverCircle(
+ cellLayer: distribution,
+ wCenter: actorPlan.WPosLocation,
+ wRadius: new WDist(actorPlan.MaxSpan() * 1024),
+ outside: false,
+ action: (mpos, _, _, _) => distribution[mpos] = 0);
+
+ ProjectPlaceDezoneActor(actorPlan, zoneable, actorDezoneRadius);
+ DezoneActor(actorPlan, clusterZoneable);
+ }
+
+ return targetCount;
+ }
+
+ ///
+ /// Chooses a location for a cluster of actors within zoneable, and then projects, places,
+ /// and dezones for them.
+ ///
+ /// Random source for locations and actor type selection.
+ /// Available space for actors. Modified if actors placed.
+ /// Actor types to choose from and their relative weights.
+ /// Number of actors to attempt to place.
+ /// Avoid placing actors' centers within this radius unless it's a last resort.
+ /// Minimum cluster radius for actor center placement.
+ /// Maximum cluster radius for actor center placement.
+ /// Zoneable spacing required beyond radius (that actors' centers will not be placed in).
+ /// If true, choose actor locations using probabilistic weights instead of best candidate.
+ ///
+ /// Dezone radius for placed actors (in addition to footprint).
+ /// This does not affect spacing within the cluster.
+ ///
+ ///
+ /// Calculates location weights or candidate priorities based on distance from the cluster
+ /// center. The input is the WDist.LengthSquared from the cluster center. Location choices
+ /// are biased towards greater outputs. If null, defaults to a function where the weight is
+ /// proportional to the squared distance, thus biasing actors towards the outside.
+ ///
+ /// Number of actors added. 0 indicates none could be added.
+ public int AddActorCluster(
+ MersenneTwister random,
+ CellLayer zoneable,
+ IReadOnlyDictionary weightedActorTypes,
+ int targetCount,
+ int innerReservation,
+ int minimumRadius,
+ int maximumRadius,
+ int outerBorder,
+ bool weighted,
+ WDist? actorDezoneRadius = null,
+ Func distributor = null)
+ {
+ CheckHasMapShape(zoneable);
+
+ var (chosenCPos, room) = ChooseInZoneable(
+ random, zoneable, maximumRadius + outerBorder);
+ var radius2 = room - outerBorder - 1;
+ if (radius2 < minimumRadius)
+ return 0;
+
+ if (radius2 > maximumRadius)
+ radius2 = maximumRadius;
+
+ var radius1 = Math.Min(innerReservation, radius2);
+ if (radius1 < 1)
+ return 0;
+
+ var distribution = new CellLayer(Map);
+ var wRadius1Sq = radius1 * radius1 * 1024L * 1024L;
+ distributor ??= wrSq => (int)(wrSq / (1024 * 1024));
+ CellLayerUtils.OverCircle(
+ cellLayer: distribution,
+ wCenter: CellLayerUtils.CPosToWPos(chosenCPos, Map.Grid.Type),
+ wRadius: new WDist(radius2 * 1024),
+ outside: false,
+ action: (mpos, _, _, wrSq) =>
+ distribution[mpos] = wrSq >= wRadius1Sq ? distributor(wrSq) : 0);
+
+ return AddDistributedActors(
+ random,
+ zoneable,
+ distribution,
+ weightedActorTypes,
+ targetCount,
+ weighted,
+ actorDezoneRadius);
+ }
+
+ ///
+ /// For a 1x1 tile, return a TerrainTile with the given tile type, using a random index if
+ /// it's a PickAny template.
+ ///
+ public TerrainTile PickTile(MersenneTwister random, ushort tileType)
+ {
+ if (templatedTerrainInfo.Templates.TryGetValue(tileType, out var template) && template.PickAny)
+ return new TerrainTile(tileType, (byte)random.Next(0, template.TilesCount));
+ else
+ return new TerrainTile(tileType, 0);
+ }
+
+ /// Wrapper around MultiBrush.PaintArea.
+ public void PaintArea(
+ MersenneTwister random,
+ CellLayer replace,
+ IReadOnlyList brushes,
+ bool alwaysPreferLargerBrushes = false)
+ {
+ CheckHasMapShape(replace);
+
+ MultiBrush.PaintArea(
+ Map,
+ ActorPlans,
+ replace,
+ brushes,
+ random,
+ alwaysPreferLargerBrushes);
+ }
+
+ ///
+ /// Wrapper around PaintArea that uses Replacibility.Actor for masked cells.
+ ///
+ public void PaintActors(
+ MersenneTwister random,
+ CellLayer mask,
+ IReadOnlyList brushes,
+ bool alwaysPreferLargerBrushes = false)
+ {
+ CheckHasMapShape(mask);
+
+ var replace = new CellLayer(Map);
+ foreach (var mpos in Map.AllCells.MapCoords)
+ replace[mpos] = mask[mpos] ? MultiBrush.Replaceability.Actor : MultiBrush.Replaceability.None;
+
+ PaintArea(
+ random,
+ replace,
+ brushes,
+ alwaysPreferLargerBrushes);
+ }
+
+ /// Wrapper around MultiBrush.Paint for path tiling results.
+ public void PaintTiling(
+ MersenneTwister random,
+ MultiBrush brush)
+ {
+ brush.Paint(Map, ActorPlans, CPos.Zero, MultiBrush.Replaceability.Any, random);
+ }
+
+ ///
+ /// Repaint the areas occupied by given tile types using MultiBrushes.
+ ///
+ public void RepaintTiles(
+ MersenneTwister random,
+ IReadOnlyDictionary> rules)
+ {
+ foreach (var (tile, collection) in rules.OrderBy(kv => kv.Key))
+ {
+ var replace = new CellLayer(Map);
+ foreach (var mpos in Map.AllCells.MapCoords)
+ replace[mpos] =
+ Map.Tiles[mpos].Type == tile
+ ? MultiBrush.Replaceability.Any
+ : MultiBrush.Replaceability.None;
+
+ MultiBrush.PaintArea(Map, ActorPlans, replace, collection, random);
+ }
+ }
+
+ ///
+ /// Creates a boolean fractal noise pattern obeying symmetry requirements.
+ /// Random source
+ /// Largest interval for fractal noise.
+ /// Target fraction of true values (from 0 to FractionMax).
+ ///
+ /// The number of times to square root the noise wavelength to arrive at the amplitude.
+ /// In other words, amplitude = wavelength ** (1 / (2 ** clumpiness))
+ /// Setting to 0 is equivalent to pink noise.
+ ///
+ ///
+ public CellLayer BooleanNoise(
+ MersenneTwister random,
+ int noiseFeatureSize,
+ int fraction,
+ int clumpiness = 0)
+ {
+ var noise = new CellLayer(Map);
+ NoiseUtils.SymmetricFractalNoiseIntoCellLayer(
+ random,
+ noise,
+ Rotations,
+ Mirror,
+ noiseFeatureSize,
+ wavelength => NoiseUtils.ClumpinessAmplitude(wavelength, clumpiness));
+
+ return CellLayerUtils.CalibratedBooleanThreshold(
+ noise, fraction, FractionMax);
+ }
+
+ ///
+ /// Create a matrix containing a generated terrain elevation map.
+ ///
+ /// Random source for terrain noise.
+ /// Largest interval for fractal noise.
+ /// Range in cells for smoothing.
+ public Matrix ElevationNoiseMatrix(
+ MersenneTwister random,
+ int noiseFeatureSize,
+ int smoothing)
+ {
+ var elevation = NoiseUtils.SymmetricFractalNoise(
+ random,
+ CellLayerUtils.CellBounds(Map).Size.ToInt2(),
+ Rotations,
+ Mirror,
+ noiseFeatureSize,
+ NoiseUtils.PinkAmplitude);
+ MatrixUtils.NormalizeRangeInPlace(elevation, 1024);
+
+ if (smoothing > 0)
+ elevation = MatrixUtils.BinomialBlur(elevation, smoothing);
+
+ return elevation;
+ }
+
+ ///
+ ///
+ /// Produce an unbiased noise pattern for resource growth.
+ ///
+ /// The output noise will have the range [uniformity, uniformity + 1024].
+ ///
+ ///
+ public CellLayer ResourceNoise(
+ MersenneTwister random,
+ int noiseFeatureSize,
+ int clumpiness,
+ int uniformity)
+ {
+ var pattern = new CellLayer(Map);
+ NoiseUtils.SymmetricFractalNoiseIntoCellLayer(
+ random,
+ pattern,
+ Rotations,
+ Mirror,
+ noiseFeatureSize,
+ wavelength => NoiseUtils.ClumpinessAmplitude(wavelength, clumpiness));
+ {
+ CellLayerUtils.CalibrateQuantileInPlace(
+ pattern,
+ 0,
+ 0, 1);
+ var max = pattern.Max();
+ foreach (var mpos in Map.AllCells.MapCoords)
+ pattern[mpos] = uniformity + 1024 * pattern[mpos] / max;
+ }
+
+ return pattern;
+ }
+
+ ///
+ /// Given elevation noise, partition it into a boolean Matrix where false represents low
+ /// elevation and true represents high elevation.
+ ///
+ /// Terrain elevation noise.
+ ///
+ /// A mask (usually a previous slice) within which the new slice is constrained to and
+ /// derived from. Can be null to imply all space is available.
+ ///
+ /// Target fraction (out of FractionMax) of masked terrain to be carried over to the new slice.
+ /// Minimum distance between the contours of the mask and the new slice.
+ public Matrix SliceElevation(
+ Matrix elevation,
+ Matrix mask,
+ int fraction,
+ int minimumContourSpacing = 0)
+ {
+ CheckHasMapShape(elevation);
+ CheckHasMapShapeOrNull(mask);
+
+ if (mask == null)
+ return MatrixUtils.CalibratedBooleanThreshold(elevation, fraction, FractionMax);
+
+ var filteredElevation = elevation.Clone();
+ var roominess = MatrixUtils.ChebyshevRoom(mask, true);
+ var available = 0;
+ var total = filteredElevation.Data.Length;
+ for (var n = 0; n < total; n++)
+ {
+ if (mask[n])
+ available++;
+ else
+ filteredElevation.Data[n] = int.MinValue;
+ }
+
+ var slice = MatrixUtils.CalibratedBooleanThreshold(
+ filteredElevation, available * fraction / FractionMax, total);
+
+ // Calibration isn't perfect. Make sure constraints are still met.
+ var minimumRoom = minimumContourSpacing + 1;
+ for (var n = 0; n < total; n++)
+ slice.Data[n] &= roominess.Data[n] >= minimumRoom;
+
+ return slice;
+ }
+
+ ///
+ /// Wrapper around InsideOutside which performs both path tiling and side filling, painting
+ /// the result to the map. If tiling fails, returns null without modifying the map.
+ ///
+ /// Random source used for tiling and filling.
+ ///
+ /// Paths to tile. Note that these are tiled exactly as specified, so if end deviation is
+ /// enabled, this will allow tiling errors.
+ ///
+ /// Side to assume if no paths are contained in the map.
+ /// If non-null, these MultiBrushes are painted over outside regions.
+ /// If non-null, these MultiBrushes are painted over inside regions.
+ /// Optional replaceability constraints for filling. Ignored for path tiling.
+ public CellLayer PaintLoopsAndFill(
+ MersenneTwister random,
+ IReadOnlyList tilingPaths,
+ Side fallback,
+ IReadOnlyList outside,
+ IReadOnlyList inside,
+ CellLayer replaceMask = null)
+ {
+ CheckHasMapShapeOrNull(replaceMask);
+
+ var tilings = new MultiBrush[tilingPaths.Count];
+ for (var i = 0; i < tilingPaths.Count; i++)
+ {
+ var tiling = tilingPaths[i].Tile(random);
+ if (tiling == null)
+ return null;
+
+ tilings[i] = tiling;
+ }
+
+ foreach (var tiling in tilings)
+ tiling.Paint(Map, ActorPlans, CPos.Zero, MultiBrush.Replaceability.Any, random);
+
+ if (inside == null && outside == null)
+ return null;
+
+ var sides = InsideOutside(tilings, fallback);
+
+ foreach (var (brushes, side) in new[] { (inside, Side.In), (outside, Side.Out) })
+ {
+ if (brushes == null)
+ continue;
+
+ var replace = new CellLayer(Map);
+ foreach (var mpos in Map.AllCells.MapCoords)
+ replace[mpos] = (sides[mpos] == side)
+ ? (replaceMask?[mpos] ?? MultiBrush.Replaceability.Any)
+ : MultiBrush.Replaceability.None;
+
+ PaintArea(random, replace, brushes);
+ }
+
+ return sides;
+ }
+
+ ///
+ /// Given a collection of path tiling results which form non-nested loops or extend beyond
+ /// or out to the map edge, return a CellLayer identifying whether cells are inside or
+ /// outside of the tiled loops, or Side.None if the cell is covered by a MultiBrush.
+ /// If a loop wraps around a space clockwise, that space is considered inside.
+ ///
+ /// Path tiling results which partition the space.
+ /// Side to assume if no paths are contained in the map.
+ public CellLayer InsideOutside(
+ IReadOnlyList tilings,
+ Side fallback)
+ {
+ var sides = new CellLayer(Map);
+ var tiledPoints = new CPos[tilings.Count][];
+ var tiledArea = new CellLayer(Map);
+ for (var i = 0; i < tilings.Count; i++)
+ {
+ tiledPoints[i] = tilings[i].Segment.Points
+ .Select(vec => CPos.Zero + vec)
+ .ToArray();
+ foreach (var cvec in tilings[i].Shape)
+ if (tiledArea.Contains(CPos.Zero + cvec))
+ tiledArea[CPos.Zero + cvec] = true;
+ }
+
+ var chiralityMatrix = MatrixUtils.PointsChirality(
+ CellLayerUtils.CellBounds(Map).Size.ToInt2(),
+ CellLayerUtils.ToMatrixPoints(tiledPoints, Map.Tiles));
+ if (chiralityMatrix == null)
+ {
+ sides.Clear(fallback);
+ return sides;
+ }
+
+ var chirality = new CellLayer(Map);
+ CellLayerUtils.FromMatrix(chirality, chiralityMatrix);
+ foreach (var mpos in Map.AllCells.MapCoords)
+ {
+ if (!tiledArea[mpos])
+ {
+ if (chirality[mpos] > 0)
+ sides[mpos] = Side.In;
+ else if (chirality[mpos] < 0)
+ sides[mpos] = Side.Out;
+ }
+ }
+
+ return sides;
+ }
+
+ ///
+ /// Fill a CellLayer with a given value to identify or undo the effects of painting sided
+ /// regions. For example, this can be used to un-paint an unplayable body of water along
+ /// with its beaches.
+ ///
+ public void FillUnmaskedSideAndBorder(
+ CellLayer mask,
+ CellLayer sides,
+ Side fillSide,
+ Action fillAction)
+ {
+ CheckHasMapShape(mask);
+ CheckHasMapShape(sides);
+
+ if (fillSide == Side.None)
+ throw new ArgumentException("fillSide was not In or Out");
+
+ var notFillSide = fillSide == Side.In ? Side.Out : Side.In;
+ var fillSeeds = CellLayerUtils.Create(Map, (MPos mpos) =>
+ sides[mpos] == fillSide &&
+ !mask[mpos] &&
+ Map.Contains(mpos));
+ fillSeeds = ImproveSymmetry(fillSeeds, false, (a, b) => a || b);
+ var fillable = CellLayerUtils.Map(sides, side => side != notFillSide);
+ CellLayerUtils.SimpleFloodFill(
+ fillable,
+ fillSeeds,
+ fillAction,
+ DirectionExts.Spread4CVec);
+ }
+
+ ///
+ /// Plan passageway cutouts that, when subtracted away from obstructions, preserve
+ /// connectivity through a given space.
+ ///
+ /// Random source for carving addition passageways to comply with maximumCutoutSpacing.
+ /// Describes the space through which connectivity needs to be preserved.
+ /// Half-thickness of passageways.
+ ///
+ /// If greater than zero, inserts additional passageways, ensuring that passageways are no
+ /// greater than this distance apart (in Chebyshev distance).
+ ///
+ public CellLayer PlanPassages(
+ MersenneTwister random,
+ CellLayer space,
+ int cutoutRadius,
+ int maximumCutoutSpacing = 0)
+ {
+ CheckHasMapShape(space);
+
+ var passages = new CellLayer(Map);
+
+ if (cutoutRadius <= 0)
+ return passages;
+
+ if (maximumCutoutSpacing > 0)
+ {
+ space = CellLayerUtils.Clone(space);
+ var roominess = new CellLayer(Map);
+ CellLayerUtils.ChebyshevRoom(roominess, space, false);
+ foreach (var mpos in Map.AllCells.MapCoords)
+ roominess[mpos] = Math.Min(
+ maximumCutoutSpacing,
+ roominess[mpos]);
+
+ while (true)
+ {
+ var (chosenMPos, room) = CellLayerUtils.FindRandomBest(
+ roominess,
+ random,
+ (a, b) => a.CompareTo(b));
+ if (room < maximumCutoutSpacing)
+ break;
+
+ var projections = Symmetry.RotateAndMirrorCPos(
+ chosenMPos.ToCPos(Map),
+ space,
+ Rotations,
+ Mirror);
+ foreach (var projection in projections)
+ {
+ if (space.Contains(projection))
+ space[projection] = false;
+ var minX = projection.X - 2 * maximumCutoutSpacing + 1;
+ var minY = projection.Y - 2 * maximumCutoutSpacing + 1;
+ var maxX = projection.X + 2 * maximumCutoutSpacing - 1;
+ var maxY = projection.Y + 2 * maximumCutoutSpacing - 1;
+ for (var y = minY; y <= maxY; y++)
+ for (var x = minX; x <= maxX; x++)
+ {
+ var mpos = new CPos(x, y).ToMPos(Map);
+ if (roominess.Contains(mpos))
+ roominess[mpos] = 0;
+ }
+ }
+ }
+ }
+
+ var matrixSpace = CellLayerUtils.ToMatrix(space, false);
+
+ // deflated is grid points, not squares. Has a size of `size + 1`.
+ var deflated = MatrixUtils.DeflateSpace(matrixSpace, false);
+ var kernel = new Matrix(2 * cutoutRadius, 2 * cutoutRadius).Fill(true);
+ var inflated = MatrixUtils.KernelDilateOrErode(deflated.Map(v => v != 0), kernel, new int2(cutoutRadius - 1, cutoutRadius - 1), true);
+ CellLayerUtils.FromMatrix(passages, inflated, true);
+
+ return passages;
+ }
+
+ ///
+ /// Plan paths for roads that travel through the middle of playable space.
+ ///
+ /// Space in which roads are permitted.
+ /// Minimum distance that roads must be from the edges of available space.
+ /// Roads shorter than this will be merged or pruned.
+ public CPos[][] PlanRoads(
+ CellLayer availableSpace,
+ int minimumSpacing,
+ int minimumLength)
+ {
+ CheckHasMapShape(availableSpace);
+
+ // For awkward symmetries, we try harder to make sure roads are fairer.
+ // This can degrade the quantity of roads, though.
+ var imperfectSymmetry =
+ Mirror != Symmetry.Mirror.None ||
+ Rotations == 3 ||
+ Rotations >= 5;
+ var gridType = Map.Grid.Type;
+
+ // Enlargement must increase dimensions by multiple of 4 to maximize compatibility
+ // with IsometricRectangular grids, where a non-multiple of 4 would change how the
+ // center aligns with the grid.
+ var enlargedSize = new Size(
+ Map.MapSize.Width + (Map.MapSize.Width & ~3) + 4,
+ Map.MapSize.Height + (Map.MapSize.Height & ~3) + 4);
+
+ var space = new CellLayer(gridType, enlargedSize);
+ space.Clear(true);
+
+ var enlargedOffset =
+ CellLayerUtils.WPosToCPos(CellLayerUtils.Center(space), gridType)
+ - CellLayerUtils.WPosToCPos(CellLayerUtils.Center(Map.Tiles), gridType);
+
+ foreach (var cpos in Map.AllCells)
+ space[cpos + enlargedOffset] = availableSpace[cpos];
+
+ space = ImproveSymmetry(space, true, (a, b) => a && b);
+
+ var matrixSpace = CellLayerUtils.ToMatrix(space, true);
+ var kernel = new Matrix(minimumSpacing * 2 + 1, minimumSpacing * 2 + 1);
+ MatrixUtils.OverCircle(
+ matrix: kernel,
+ centerIn1024ths: kernel.Size * 512,
+ radiusIn1024ths: minimumSpacing * 1024,
+ outside: false,
+ action: (xy, _) => kernel[xy] = true);
+ var dilated = MatrixUtils.KernelDilateOrErode(
+ matrixSpace,
+ kernel,
+ new int2(minimumSpacing, minimumSpacing),
+ false);
+ var deflated = MatrixUtils.DeflateSpace(dilated, true);
+
+ if (imperfectSymmetry)
+ {
+ var changing = true;
+ while (changing)
+ {
+ changing = false;
+
+ // Delete short paths.
+ {
+ MatrixUtils.RemoveStubsFromDirectionMapInPlace(deflated);
+ var paths = MatrixUtils.DirectionMapToPaths(deflated);
+ if (paths.Length == 0)
+ break;
+
+ var minLength = paths.Min(p => p.Length);
+ if (minLength < minimumLength)
+ {
+ changing = true;
+ var shortPaths = paths
+ .Where(path => path.Length == minLength);
+ foreach (var path in shortPaths)
+ foreach (var point in path)
+ deflated[point] = 0;
+ MatrixUtils.RemoveStubsFromDirectionMapInPlace(deflated);
+ }
+ }
+
+ // Prune asymmetric paths.
+ {
+ const int Dilation = 3;
+ var nearPath = MatrixUtils.KernelDilateOrErode(
+ deflated.Map(v => v != 0),
+ new Matrix(Dilation * 2 + 1, Dilation * 2 + 1).Fill(true),
+ new int2(Dilation, Dilation),
+ true);
+ var matrixPaths = MatrixUtils.DirectionMapToPaths(deflated);
+ foreach (var path in matrixPaths)
+ {
+ var cposPath = CellLayerUtils.FromMatrixPoints([path], space)[0];
+ var projectedPoints = cposPath
+ .SelectMany(p => Symmetry.RotateAndMirrorCPos(p, space, Rotations, Mirror))
+ .ToArray();
+ var matrixPoints = CellLayerUtils.ToMatrixPoints([projectedPoints], space)[0];
+ if (!matrixPoints.All(p => !nearPath.ContainsXY(p) || nearPath[p]))
+ {
+ // The path doesn't exist across all symmetries (or isn't consistent enough).
+ changing = true;
+ foreach (var point in path)
+ deflated[point] = 0;
+ }
+ }
+ }
+ }
+ }
+
+ var matrixPointArrays = MatrixUtils.DirectionMapToPathsWithPruning(
+ input: deflated,
+ minimumLength: minimumLength,
+ minimumJunctionSeparation: 6,
+ preserveEdgePaths: true);
+ var pointArrays = CellLayerUtils.FromMatrixPoints(matrixPointArrays, space);
+ pointArrays = TilingPath.RetainDisjointPaths(pointArrays);
+ pointArrays = pointArrays
+ .Select(a => a.Select(p => p - enlargedOffset).ToArray())
+ .Select(a => TilingPath.ChirallyNormalizePathPoints(a, cvec => CellLayerUtils.CornerToWPos(cvec, gridType) - CellLayerUtils.Center(Map)))
+ .ToArray();
+
+ return pointArrays;
+ }
+
+ ///
+ /// Given a resource noise pattern, rank cells for resource growth. (Higher is better.)
+ /// Resources will be limited to masked cells. Resources will only be placed on compatible
+ /// terrain tiles and will avoid actor footprints.
+ /// Resources can be biased towards or away from specified actors. Biases are applied in
+ /// the order they are supplied, but all reservations take precedence.
+ /// Resource type will be determined by proximity to resource spawn actors, or a default
+ /// resource.
+ ///
+ public (CellLayer Plan, CellLayer TypePlan) PlanResources(
+ CellLayer pattern,
+ CellLayer mask,
+ ResourceTypeInfo defaultResource,
+ IReadOnlyList resourceBiases)
+ {
+ CheckHasMapShape(pattern);
+ CheckHasMapShape(mask);
+
+ // IReadOnlyDictionary resourceSpawnSeeds = ...;
+ var resourceTypes = Map.Rules.Actors[SystemActors.World]
+ .TraitInfoOrDefault()
+ .ResourceTypes
+ .OrderBy(kv => kv.Key)
+ .Select(kv => kv.Value)
+ .ToImmutableArray();
+ var allowedTerrainResourceCombos = resourceTypes
+ .SelectMany(resourceTypeInfo => resourceTypeInfo.AllowedTerrainTypes
+ .Select(terrainName => (resourceTypeInfo, terrainInfo.GetTerrainIndex(terrainName))))
+ .ToImmutableHashSet();
+
+ var strengths = new Dictionary>();
+ foreach (var resourceType in resourceTypes)
+ {
+ var strength = new CellLayer(Map);
+ strength.Clear(1);
+ strengths.Add(resourceType, strength);
+ }
+
+ foreach (var bias in resourceBiases)
+ {
+ if (bias.Bias == null || bias.BiasRadius == null)
+ continue;
+
+ IEnumerable types = bias.ResourceType != null
+ ? [bias.ResourceType]
+ : resourceTypes;
+ foreach (var resourceType in types)
+ {
+ var strength = strengths[resourceType];
+ CellLayerUtils.OverCircle(
+ cellLayer: strength,
+ wCenter: bias.WPos,
+ wRadius: bias.BiasRadius.Value,
+ outside: false,
+ action: (mpos, _, _, wrSq) =>
+ strength[mpos] = bias.Bias(strength[mpos], wrSq));
+ }
+ }
+
+ var maxStrength1024ths = new CellLayer(Map);
+ maxStrength1024ths.Clear(1);
+ var bestResource = new CellLayer(Map);
+ bestResource.Clear(defaultResource);
+ foreach (var resourceStrength in strengths)
+ {
+ var resource = resourceStrength.Key;
+ var strength1024ths = resourceStrength.Value;
+ foreach (var mpos in Map.AllCells.MapCoords)
+ if (strength1024ths[mpos] > maxStrength1024ths[mpos])
+ {
+ maxStrength1024ths[mpos] = strength1024ths[mpos];
+ bestResource[mpos] = resource;
+ }
+ }
+
+ // Closer to +inf means "more preferable" for plan.
+ var plan = new CellLayer(Map);
+ foreach (var mpos in Map.AllCells.MapCoords)
+ {
+ plan[mpos] = pattern[mpos] >= 0
+ ? pattern[mpos] * maxStrength1024ths[mpos]
+ : -int.MaxValue;
+ }
+
+ foreach (var mpos in Map.AllCells.MapCoords)
+ if (!mask[mpos] || !allowedTerrainResourceCombos.Contains((bestResource[mpos], Map.GetTerrainIndex(mpos))))
+ plan[mpos] = -int.MaxValue;
+
+ foreach (var bias in resourceBiases)
+ {
+ if (bias.ExclusionRadius == null)
+ continue;
+
+ foreach (var resourceType in resourceTypes)
+ {
+ CellLayerUtils.OverCircle(
+ cellLayer: plan,
+ wCenter: bias.WPos,
+ wRadius: bias.ExclusionRadius.Value,
+ outside: false,
+ action: (mpos, _, _, wrSq) =>
+ plan[mpos] = -int.MaxValue);
+ }
+ }
+
+ plan = ImproveSymmetry(plan, -int.MaxValue, int.Min);
+
+ return (plan, bestResource);
+ }
+
+ ///
+ /// Given a resource plan, place resources onto the map up to a target value.
+ /// Resources are placed first on the pattern cells with the greatest value.
+ /// No resources will be placed on pattern cells with a value less than 0.
+ /// The plan should only contain values >= 0 where resource placement is legal.
+ /// The type of resource placed is specified by typePlan.
+ /// Any previously existing resources on the map will be cleared.
+ ///
+ public void GrowResources(
+ CellLayer plan,
+ CellLayer typePlan,
+ long targetValue)
+ {
+ CheckHasMapShape(plan);
+ CheckHasMapShape(typePlan);
+
+ var remaining = targetValue;
+
+ var resourceTypes = Map.Rules.Actors[SystemActors.World].TraitInfoOrDefault().ResourceTypes;
+ var playerResourcesInfo = Map.Rules.Actors[SystemActors.Player].TraitInfoOrDefault();
+ var resourceValues = playerResourcesInfo.ResourceValues
+ .ToDictionary(kv => resourceTypes[kv.Key], kv => kv.Value);
+
+ // Closer to -inf means "more preferable" for priorities.
+ var priorities = new PriorityArray(
+ plan.Size.Width * plan.Size.Height,
+ int.MaxValue);
+ {
+ var i = 0;
+ foreach (var v in plan)
+ priorities[i++] = -v;
+ }
+
+ int PriorityIndex(MPos mpos) => mpos.V * plan.Size.Width + mpos.U;
+ MPos PriorityMPos(int index)
+ {
+ var v = Math.DivRem(index, plan.Size.Width, out var u);
+ return new MPos(u, v);
+ }
+
+ Map.Resources.Clear();
+
+ // Return resource value of a given square.
+ // Matches the logic in ResourceLayer trait.
+ int CheckValue(CPos cpos)
+ {
+ if (!Map.Resources.Contains(cpos))
+ return 0;
+ var resource = Map.Resources[cpos].Type;
+ if (resource == 0)
+ return 0;
+
+ var resourceType = typePlan[cpos];
+
+ var adjacent = 0;
+ var directions = CVec.Directions;
+ for (var i = 0; i < directions.Length; i++)
+ {
+ var c = cpos + directions[i];
+ if (Map.Resources.Contains(c) && Map.Resources[c].Type == resource)
+ ++adjacent;
+ }
+
+ // We need to have at least one resource in the cell.
+ // HACK: we should not be lerping to 9, as maximum adjacent resources is 8.
+ // HACK: it's too disruptive to fix.
+ var density = Math.Max(int2.Lerp(0, resourceType.MaxDensity, adjacent, 9), 1);
+
+ return resourceValues[resourceType] * density;
+ }
+
+ int CheckValue3By3(CPos cpos)
+ {
+ var total = 0;
+ for (var y = -1; y <= 1; y++)
+ for (var x = -1; x <= 1; x++)
+ total += CheckValue(cpos + new CVec(x, y));
+
+ return total;
+ }
+
+ var gridType = Map.Grid.Type;
+
+ // Set and return change in overall value.
+ int AddResource(CPos cpos)
+ {
+ var mpos = cpos.ToMPos(gridType);
+ priorities[PriorityIndex(mpos)] = int.MaxValue;
+
+ // Generally shouldn't happen, but perhaps a rotation/mirror related inaccuracy.
+ if (Map.Resources[mpos].Type != 0)
+ return 0;
+
+ var resourceType = typePlan[mpos];
+ var oldValue = CheckValue3By3(cpos);
+ Map.Resources[mpos] = new ResourceTile(
+ resourceType.ResourceIndex,
+ (byte)resourceType.MaxDensity);
+ var newValue = CheckValue3By3(cpos);
+ return newValue - oldValue;
+ }
+
+ while (remaining > 0)
+ {
+ var n = priorities.GetMinIndex();
+ if (priorities[n] == int.MaxValue)
+ break;
+
+ var chosenMPos = PriorityMPos(n);
+ var chosenCPos = chosenMPos.ToCPos(gridType);
+ foreach (var cpos in Symmetry.RotateAndMirrorCPos(chosenCPos, plan, Rotations, Mirror))
+ if (Map.Resources.Contains(cpos))
+ remaining -= AddResource(cpos);
+ }
+ }
+
+ ///
+ /// Create a mask for placing decorations in out-of-the-way locations on a map.
+ ///
+ /// Random source for layout and tiling.
+ /// Space that decorations must not significantly choke.
+ /// Cells where decoration is allowed.
+ /// Maximum fraction of map to cover in decorations.
+ /// Noise feature size for layout.
+ /// Density of decoration layout.
+ ///
+ /// Enforces a minimum local density of decorations. This can, for example, be used to
+ /// ensure that villages have a substantial size, preventing lonely buildings. Decoration
+ /// cells are removed until the minimum density is satisfied for remaining cells.
+ ///
+ /// Enforcement radius of minimum density.
+ public CellLayer DecorationPattern(
+ MersenneTwister random,
+ CellLayer space,
+ CellLayer zoneable,
+ int coverage,
+ int featureSize,
+ int density,
+ int minimumDensity,
+ int minimumDensityRadius)
+ {
+ CheckHasMapShape(space);
+ CheckHasMapShape(zoneable);
+
+ var matrixSpace = CellLayerUtils.ToMatrix(space, true);
+ var deflated = MatrixUtils.DeflateSpace(matrixSpace, false);
+ var kernel = new Matrix(2, 2).Fill(true);
+ var reservedMatrix = MatrixUtils.KernelDilateOrErode(deflated.Map(v => v != 0), kernel, new int2(0, 0), true);
+ var reserved = new CellLayer(Map);
+ CellLayerUtils.FromMatrix(reserved, reservedMatrix, true);
+
+ var decorationNoise = new CellLayer(Map);
+ NoiseUtils.SymmetricFractalNoiseIntoCellLayer(
+ random,
+ decorationNoise,
+ Rotations,
+ Mirror,
+ featureSize,
+ NoiseUtils.WhiteAmplitude);
+
+ var densityNoise = new CellLayer(Map);
+ NoiseUtils.SymmetricFractalNoiseIntoCellLayer(
+ random,
+ densityNoise,
+ Rotations,
+ Mirror,
+ 1024,
+ NoiseUtils.PinkAmplitude);
+ var densityMask = CellLayerUtils.CalibratedBooleanThreshold(
+ densityNoise, density, FractionMax);
+
+ var decorable = new CellLayer(Map);
+ var totalDecorable = 0;
+ foreach (var mpos in Map.AllCells.MapCoords)
+ {
+ var isDecorable =
+ zoneable[mpos] && space[mpos] && !reserved[mpos] && densityMask[mpos];
+ decorable[mpos] = isDecorable;
+ if (isDecorable)
+ totalDecorable++;
+ else
+ decorationNoise[mpos] = -1024 * 1024;
+ }
+
+ var mapArea = Map.MapSize.Width * Map.MapSize.Height;
+ var decorationMask = CellLayerUtils.CalibratedBooleanThreshold(
+ decorationNoise, totalDecorable * coverage / FractionMax, mapArea);
+ foreach (var mpos in Map.AllCells.MapCoords)
+ decorable[mpos] &= decorationMask[mpos];
+
+ for (var i = 0; i < 8; i++)
+ {
+ var (blurred, changes) = MatrixUtils.BooleanBlur(
+ CellLayerUtils.ToMatrix(decorable, false),
+ minimumDensityRadius,
+ FractionMax - minimumDensity, FractionMax);
+ if (changes == 0)
+ break;
+
+ var densityFilter = new CellLayer(Map);
+ CellLayerUtils.FromMatrix(densityFilter, blurred);
+
+ foreach (var mpos in Map.AllCells.MapCoords)
+ decorable[mpos] &= densityFilter[mpos];
+ }
+
+ decorable = ImproveSymmetry(decorable, false, (a, b) => a && b);
+
+ return decorable;
+ }
+ }
+}
diff --git a/OpenRA.Mods.Common/MapGenerator/TilingPath.cs b/OpenRA.Mods.Common/MapGenerator/TilingPath.cs
index a81f8b939d..5feba7a985 100644
--- a/OpenRA.Mods.Common/MapGenerator/TilingPath.cs
+++ b/OpenRA.Mods.Common/MapGenerator/TilingPath.cs
@@ -241,6 +241,53 @@ namespace OpenRA.Mods.Common.MapGenerator
Brushes = permittedTemplates;
}
+ ///
+ /// 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.
+ ///
+ public static TilingPath QuickCreate(
+ Map map,
+ IReadOnlyList 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;
}
+ /// Applies StraightenEndsPathPoints to this TilingPath, returning this.
+ public TilingPath StraightenEnds(
+ int shrink,
+ int grow,
+ int minimumLength,
+ int growthInertialRange)
+ {
+ Points = StraightenEndsPathPoints(
+ Points,
+ CellLayerUtils.CellBounds(Map),
+ shrink,
+ grow,
+ minimumLength,
+ growthInertialRange);
+ return this;
+ }
+
+ ///
+ /// Straighten the start and end of a path by shrinking and regrowing a straight section.
+ ///
+ /// Points of the path.
+ /// Map bounds, used to identify paths touching edges.
+ /// Distance to shrink path ends (before regrowing them).
+ /// Distance to regrow path ends with straightening (after shrinking).
+ /// The minimum length (after shrinking, before growth) that paths may be.
+ /// How many points are used to decide the regrowth direction.
+ 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;
+ }
+
/// Set MaxEndDeviation.
public TilingPath SetMaxEndDeviation(int maxEndDeviation)
{
diff --git a/OpenRA.Mods.Common/Traits/World/ClearMapGenerator.cs b/OpenRA.Mods.Common/Traits/World/ClearMapGenerator.cs
index 63a40590ed..d2381a4f0b 100644
--- a/OpenRA.Mods.Common/Traits/World/ClearMapGenerator.cs
+++ b/OpenRA.Mods.Common/Traits/World/ClearMapGenerator.cs
@@ -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 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;
}
diff --git a/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs b/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs
index d586337feb..1871e4ceeb 100644
--- a/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs
+++ b/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs
@@ -15,7 +15,6 @@ using System.Collections.Immutable;
using System.Linq;
using OpenRA.Mods.Common.MapGenerator;
using OpenRA.Mods.Common.Terrain;
-using OpenRA.Primitives;
using OpenRA.Support;
using OpenRA.Traits;
using static OpenRA.Mods.Common.Traits.ResourceLayerInfo;
@@ -67,7 +66,7 @@ namespace OpenRA.Mods.Common.Traits
.Options.SelectMany(o => o.GetFluentReferences()).ToList();
}
- const int FractionMax = 1000;
+ const int FractionMax = Terraformer.FractionMax;
const int EntityBonusMax = 1000000;
sealed class Parameters
@@ -200,15 +199,9 @@ namespace OpenRA.Mods.Common.Traits
[FieldLoader.Ignore]
public readonly IReadOnlyDictionary> RepaintTiles;
- [FieldLoader.Ignore]
- public readonly IReadOnlyDictionary ResourceTypes;
[FieldLoader.Ignore]
public readonly ResourceTypeInfo DefaultResource;
[FieldLoader.Ignore]
- public readonly IReadOnlyDictionary ResourceValues;
- [FieldLoader.Ignore]
- public readonly IReadOnlySet<(ResourceTypeInfo, byte)> AllowedTerrainResourceCombos;
- [FieldLoader.Ignore]
public readonly IReadOnlyDictionary ResourceSpawnSeeds;
[FieldLoader.LoadUsing(nameof(ResourceSpawnWeightsLoader))]
public readonly IReadOnlyDictionary ResourceSpawnWeights = default;
@@ -218,16 +211,10 @@ namespace OpenRA.Mods.Common.Traits
[FieldLoader.Ignore]
public readonly IReadOnlySet PlayableTerrain;
[FieldLoader.Ignore]
- public readonly IReadOnlySet PartiallyPlayableTerrain;
- [FieldLoader.Ignore]
- public readonly IReadOnlySet UnplayableTerrain;
- [FieldLoader.Ignore]
public readonly IReadOnlySet DominantTerrain;
[FieldLoader.Ignore]
public readonly IReadOnlySet ZoneableTerrain;
[FieldLoader.Ignore]
- public readonly IReadOnlySet PartiallyPlayableCategories;
- [FieldLoader.Ignore]
public readonly IReadOnlyList ClearSegmentTypes;
[FieldLoader.Ignore]
public readonly IReadOnlyList BeachSegmentTypes;
@@ -256,22 +243,15 @@ namespace OpenRA.Mods.Common.Traits
v => MultiBrush.LoadCollection(map, v.Value) as IReadOnlyList);
RepaintTiles ??= ImmutableDictionary>.Empty;
- ResourceTypes = map.Rules.Actors[SystemActors.World].TraitInfoOrDefault().ResourceTypes;
- if (!ResourceTypes.TryGetValue(my.NodeWithKey("DefaultResource").Value.Value, out DefaultResource))
+ var resourceTypes = map.Rules.Actors[SystemActors.World].TraitInfoOrDefault().ResourceTypes;
+ if (!resourceTypes.TryGetValue(my.NodeWithKey("DefaultResource").Value.Value, out DefaultResource))
throw new YamlException("DefaultResource is not valid");
var playerResourcesInfo = map.Rules.Actors[SystemActors.Player].TraitInfoOrDefault();
- ResourceValues = playerResourcesInfo.ResourceValues
- .ToDictionary(kv => ResourceTypes[kv.Key], kv => kv.Value);
- AllowedTerrainResourceCombos = ResourceTypes
- .Values
- .SelectMany(resourceTypeInfo => resourceTypeInfo.AllowedTerrainTypes
- .Select(terrainName => (resourceTypeInfo, terrainInfo.GetTerrainIndex(terrainName))))
- .ToImmutableHashSet();
try
{
ResourceSpawnSeeds = my.NodeWithKey("ResourceSpawnSeeds").Value
.ToDictionary(subMy => subMy.Value)
- .ToDictionary(kv => kv.Key, kv => ResourceTypes[kv.Value]);
+ .ToDictionary(kv => kv.Key, kv => resourceTypes[kv.Value]);
}
catch (KeyNotFoundException e)
{
@@ -306,15 +286,9 @@ namespace OpenRA.Mods.Common.Traits
ClearTerrain = ParseTerrainIndexes("ClearTerrain");
PlayableTerrain = ParseTerrainIndexes("PlayableTerrain");
- PartiallyPlayableTerrain = ParseTerrainIndexes("PartiallyPlayableTerrain");
- UnplayableTerrain = ParseTerrainIndexes("UnplayableTerrain");
DominantTerrain = ParseTerrainIndexes("DominantTerrain");
ZoneableTerrain = ParseTerrainIndexes("ZoneableTerrain");
- PartiallyPlayableCategories = my.NodeWithKey("PartiallyPlayableCategories").Value.Value
- .Split(',', StringSplitOptions.RemoveEmptyEntries)
- .ToImmutableHashSet();
-
ClearSegmentTypes = ParseSegmentTypes("ClearSegmentTypes");
BeachSegmentTypes = ParseSegmentTypes("BeachSegmentTypes");
CliffSegmentTypes = ParseSegmentTypes("CliffSegmentTypes");
@@ -475,18 +449,6 @@ namespace OpenRA.Mods.Common.Traits
if (Players % symmetryCount != 0)
throw new MapGenerationException($"Total number of players must be a multiple of {symmetryCount}");
}
-
- public static (T[] Types, int[] Weights) SplitWeights(IReadOnlyDictionary typeWeights)
- {
- var types = typeWeights
- .Select(kv => kv.Key)
- .Order()
- .ToArray();
- var weights = types
- .Select(type => typeWeights[type])
- .ToArray();
- return (types, weights);
- }
}
public IMapGeneratorSettings GetSettings()
@@ -496,83 +458,41 @@ namespace OpenRA.Mods.Common.Traits
public Map Generate(ModData modData, MapGenerationArgs args)
{
- const int ExternalBias = 4096;
-
var terrainInfo = modData.DefaultTerrainInfo[args.Tileset];
var size = args.Size;
var map = new Map(modData, terrainInfo, size);
- var maxTerrainHeight = map.Grid.MaximumTerrainHeight;
- var tl = new PPos(1, 1 + maxTerrainHeight);
- var br = new PPos(size.Width - 1, size.Height + maxTerrainHeight - 1);
- map.SetBounds(tl, br);
- map.Title = args.Title;
- map.Author = args.Author;
- map.RequiresMod = modData.Manifest.Id;
-
- var minSpan = Math.Min(size.Width, size.Height);
- var mapCenter1024ths = new int2(size.Width * 512, size.Height * 512);
- var wMapCenter = CellLayerUtils.Center(map.Tiles);
- var matrixMapCenter1024ths = CellLayerUtils.CellBounds(map).Size.ToInt2() * 512;
- var cellBounds = CellLayerUtils.CellBounds(map);
- var minCSpan = Math.Min(cellBounds.Size.Width, cellBounds.Size.Height);
- var gridType = map.Grid.Type;
-
var actorPlans = new List();
var param = new Parameters(map, args.Settings);
- var externalCircleRadius = minCSpan / 2 - (param.MinimumLandSeaThickness + param.MinimumMountainThickness);
- if (externalCircleRadius <= 0)
+ var terraformer = new Terraformer(args, map, modData, actorPlans, param.Mirror, param.Rotations);
+
+ var waterIsPlayable = param.PlayableTerrain.Contains(terrainInfo.GetTerrainIndex(new TerrainTile(param.WaterTile, 0)));
+
+ var externalCircleRadius = CellLayerUtils.Radius(map) - new WDist((param.MinimumLandSeaThickness + param.MinimumMountainThickness) * 1024);
+ if (param.ExternalCircularBias != 0 && externalCircleRadius.Length <= 0)
throw new MapGenerationException("map is too small for circular shaping");
- var beachPermittedTemplates = TilingPath.PermittedSegments.FromType(param.SegmentedBrushes, param.BeachSegmentTypes);
- var replaceabilityMap = new Dictionary();
- var playabilityMap = new Dictionary();
-
- var templatedTerrainInfo = (ITemplatedTerrainInfo)terrainInfo;
- foreach (var kv in templatedTerrainInfo.Templates)
+ CellLayer PlayableToReplaceable()
{
- var id = kv.Key;
- var template = kv.Value;
- for (var ti = 0; ti < template.TilesCount; ti++)
- {
- if (template[ti] == null)
- continue;
- var tile = new TerrainTile(id, (byte)ti);
- var type = terrainInfo.GetTerrainIndex(tile);
-
- if (param.PlayableTerrain.Contains(type))
- playabilityMap[tile] = PlayableSpace.Playability.Playable;
- else if (param.PartiallyPlayableTerrain.Contains(type))
- playabilityMap[tile] = PlayableSpace.Playability.Partial;
- else if (param.UnplayableTerrain.Contains(type))
- playabilityMap[tile] = PlayableSpace.Playability.Unplayable;
- else
- throw new MapGenerationException($"Terrain index {type} has unknown playability.");
-
- if (id == param.LandTile)
+ var playable = terraformer.CheckSpace(param.PlayableTerrain, true);
+ var basicLand = terraformer.CheckSpace(param.LandTile);
+ var replace = new CellLayer(map);
+ foreach (var mpos in map.AllCells.MapCoords)
+ if (playable[mpos])
{
- replaceabilityMap[tile] = MultiBrush.Replaceability.Any;
- }
- else if (id == param.WaterTile)
- {
- replaceabilityMap[tile] = MultiBrush.Replaceability.Tile;
- }
- else
- {
- if (playabilityMap[tile] == PlayableSpace.Playability.Unplayable)
- replaceabilityMap[tile] = MultiBrush.Replaceability.None;
+ if (basicLand[mpos])
+ replace[mpos] = MultiBrush.Replaceability.Any;
else
- replaceabilityMap[tile] = MultiBrush.Replaceability.Actor;
-
- if (param.PartiallyPlayableCategories.Overlaps(template.Categories)
- && playabilityMap[tile] == PlayableSpace.Playability.Unplayable)
- {
- playabilityMap[tile] = PlayableSpace.Playability.Partial;
- }
+ replace[mpos] = MultiBrush.Replaceability.Actor;
}
- }
+ else
+ {
+ replace[mpos] = MultiBrush.Replaceability.None;
+ }
+
+ return replace;
}
// Use `random` to derive separate independent random number generators.
@@ -585,7 +505,7 @@ namespace OpenRA.Mods.Common.Traits
// random.Next(). All generators should be created unconditionally.
var random = new MersenneTwister(param.Seed);
- var waterRandom = new MersenneTwister(random.Next());
+ var elevationRandom = new MersenneTwister(random.Next());
var beachTilingRandom = new MersenneTwister(random.Next());
var cliffTilingRandom = new MersenneTwister(random.Next());
var forestRandom = new MersenneTwister(random.Next());
@@ -603,197 +523,91 @@ namespace OpenRA.Mods.Common.Traits
var decorationTilingRandom = new MersenneTwister(random.Next());
var pickAnyRandom = new MersenneTwister(random.Next());
- TerrainTile PickTile(ushort tileType)
- {
- if (templatedTerrainInfo.Templates.TryGetValue(tileType, out var template) && template.PickAny)
- return new TerrainTile(tileType, (byte)random.Next(0, template.TilesCount));
- else
- return new TerrainTile(tileType, 0);
- }
+ terraformer.InitMap();
- foreach (var cell in map.AllCells)
- {
- var mpos = cell.ToMPos(gridType);
- map.Tiles[mpos] = PickTile(param.LandTile);
- map.Resources[mpos] = new ResourceTile(0, 0);
- map.Height[mpos] = 0;
- }
+ foreach (var mpos in map.AllCells.MapCoords)
+ map.Tiles[mpos] = terraformer.PickTile(pickAnyRandom, param.LandTile);
- var elevation = NoiseUtils.SymmetricFractalNoise(
- waterRandom,
- cellBounds.Size.ToInt2(),
- param.Rotations,
- param.Mirror,
+ var elevation = terraformer.ElevationNoiseMatrix(
+ elevationRandom,
param.TerrainFeatureSize,
- NoiseUtils.PinkAmplitude);
- MatrixUtils.NormalizeRangeInPlace(elevation, 1024);
+ param.TerrainSmoothing);
- if (param.TerrainSmoothing > 0)
+ Matrix mapShape;
+ if (param.ExternalCircularBias == 0)
+ mapShape = new Matrix(CellLayerUtils.CellBounds(map).Size.ToInt2()).Fill(true);
+ else
+ mapShape = CellLayerUtils.ToMatrix(terraformer.CenteredCircle(true, false, externalCircleRadius), false);
+
+ var landPlan = terraformer.SliceElevation(elevation, mapShape, FractionMax - param.Water);
+
+ if (param.ExternalCircularBias > 0)
{
- var radius = param.TerrainSmoothing;
- elevation = MatrixUtils.BinomialBlur(elevation, radius);
+ for (var n = 0; n < landPlan.Data.Length; n++)
+ landPlan[n] |= !mapShape[n];
+ var ring = terraformer.CenteredCircle(false, true, externalCircleRadius + new WDist(param.MinimumMountainThickness * 1024));
+ var path = TilingPath.QuickCreate(
+ map,
+ param.SegmentedBrushes,
+ CellLayerUtils.BordersToPoints(ring)[0],
+ (param.MinimumMountainThickness - 1) / 2,
+ param.CliffSegmentTypes[0],
+ param.CliffSegmentTypes[0]);
+ var brush = path.Tile(cliffTilingRandom)
+ ?? throw new MapGenerationException("Could not fit tiles for exterior circle cliffs");
+ terraformer.PaintTiling(pickAnyRandom, brush);
}
- MatrixUtils.CalibrateQuantileInPlace(
- elevation,
- 0,
- param.Water, FractionMax);
-
- if (param.ExternalCircularBias != 0)
- MatrixUtils.OverCircle(
- matrix: elevation,
- centerIn1024ths: mapCenter1024ths,
- radiusIn1024ths: externalCircleRadius * 1024,
- outside: true,
- action: (xy, _) => elevation[xy] = param.ExternalCircularBias * ExternalBias);
-
- var landPlan = MatrixUtils.BooleanBlotch(
- elevation.Map(v => v >= 0),
+ landPlan = MatrixUtils.BooleanBlotch(
+ landPlan,
param.TerrainSmoothing,
- param.SmoothingThreshold, FractionMax,
+ param.SmoothingThreshold, /*smoothingThresholdOutOf=*/FractionMax,
param.MinimumLandSeaThickness,
- /*bias=*/param.Water < FractionMax / 2);
+ /*bias=*/param.Water <= FractionMax / 2);
var beaches = CellLayerUtils.FromMatrixPoints(
MatrixUtils.BordersToPoints(landPlan),
map.Tiles);
- var beachesShape = new HashSet();
- if (beaches.Length > 0)
- {
- var tiledBeaches = new CPos[beaches.Length][];
- for (var i = 0; i < beaches.Length; i++)
- {
- var beachPath = new TilingPath(
- map,
- beaches[i],
- (param.MinimumLandSeaThickness - 1) / 2,
- param.BeachSegmentTypes[0],
- param.BeachSegmentTypes[0],
- beachPermittedTemplates);
- beachPath
- .ExtendEdge(4)
- .SetAutoEndDeviation()
- .OptimizeLoop();
- var brush = beachPath.Tile(beachTilingRandom)
- ?? throw new MapGenerationException("Could not fit tiles for beach");
- brush.Paint(map, actorPlans, CPos.Zero, MultiBrush.Replaceability.Tile, pickAnyRandom);
- tiledBeaches[i] = brush.Segment.Points.Select(vec => CPos.Zero + vec).ToArray();
- foreach (var cvec in brush.Shape)
- beachesShape.Add(CPos.Zero + cvec);
- }
-
- var beachChiralityMatrix = MatrixUtils.PointsChirality(
- landPlan.Size,
- CellLayerUtils.ToMatrixPoints(tiledBeaches, map.Tiles));
- var beachChirality = new CellLayer(map);
- CellLayerUtils.FromMatrix(beachChirality, beachChiralityMatrix);
- foreach (var mpos in map.AllCells.MapCoords)
- {
- // `map.Tiles[mpos].Type == param.LandTile` avoids overwriting beach tiles.
- if (beachChirality[mpos] < 0 && !beachesShape.Contains(mpos.ToCPos(map)))
- map.Tiles[mpos] = PickTile(param.WaterTile);
- }
- }
- else
- {
- // There weren't any coastlines
- var tileType = landPlan[0] ? param.LandTile : param.WaterTile;
- foreach (var cell in map.AllCells)
- {
- var mpos = cell.ToMPos(gridType);
- map.Tiles[mpos] = PickTile(tileType);
- }
- }
-
- var nonLoopedCliffPermittedTemplates =
- TilingPath.PermittedSegments.FromInnerAndTerminalTypes(
- param.SegmentedBrushes, param.CliffSegmentTypes, param.ClearSegmentTypes);
- var loopedCliffPermittedTemplates =
- TilingPath.PermittedSegments.FromType(
- param.SegmentedBrushes, param.CliffSegmentTypes);
- if (param.ExternalCircularBias > 0)
- {
- var cliffRing = new CellLayer(map);
- CellLayerUtils.OverCircle(
- cellLayer: cliffRing,
- wCenter: wMapCenter,
- wRadius: (externalCircleRadius + param.MinimumMountainThickness) * 1024,
- outside: true,
- action: (mpos, _, _, _) => cliffRing[mpos] = true);
- foreach (var cliff in CellLayerUtils.BordersToPoints(cliffRing))
- {
- var isLoop = cliff[0] == cliff[^1];
- TilingPath cliffPath;
- if (isLoop)
- cliffPath = new TilingPath(
+ var beachPaths = beaches
+ .Select(beach =>
+ TilingPath.QuickCreate(
map,
- cliff,
- (param.MinimumMountainThickness - 1) / 2,
- param.CliffSegmentTypes[0],
- param.CliffSegmentTypes[0],
- loopedCliffPermittedTemplates);
- else
- cliffPath = new TilingPath(
- map,
- cliff,
- (param.MinimumMountainThickness - 1) / 2,
- param.ClearSegmentTypes[0],
- param.ClearSegmentTypes[0],
- nonLoopedCliffPermittedTemplates);
- cliffPath
- .ExtendEdge(4)
- .SetAutoEndDeviation()
- .OptimizeLoop();
- var brush = cliffPath.Tile(cliffTilingRandom)
- ?? throw new MapGenerationException("Could not fit tiles for exterior circle cliffs");
- brush.Paint(map, actorPlans, CPos.Zero, MultiBrush.Replaceability.Tile, pickAnyRandom);
- }
- }
+ param.SegmentedBrushes,
+ beach,
+ (param.MinimumLandSeaThickness - 1) / 2,
+ param.BeachSegmentTypes[0],
+ param.BeachSegmentTypes[0])
+ .ExtendEdge(4))
+ .ToArray();
+ var landBeachWater = terraformer.PaintLoopsAndFill(
+ beachTilingRandom,
+ beachPaths,
+ landPlan[0] ? Terraformer.Side.In : Terraformer.Side.Out,
+ [new MultiBrush().WithTemplate(map, param.WaterTile, CVec.Zero)],
+ null)
+ ?? throw new MapGenerationException("Could not fit tiles for beach");
- if (param.Mountains > 0 || param.ExternalCircularBias == 1)
+ if (param.Mountains > 0)
{
var roughnessMatrix = MatrixUtils.GridVariance(
elevation,
param.RoughnessRadius);
- MatrixUtils.CalibrateQuantileInPlace(
+ var cliffMask = MatrixUtils.CalibratedBooleanThreshold(
roughnessMatrix,
- 0,
- FractionMax - param.Roughness, FractionMax);
- var cliffMask = roughnessMatrix.Map(v => v >= 0);
- var mountainElevation = elevation.Clone();
- var cliffPlan = landPlan;
- if (param.ExternalCircularBias > 0)
- MatrixUtils.OverCircle(
- matrix: cliffPlan,
- centerIn1024ths: matrixMapCenter1024ths,
- radiusIn1024ths: externalCircleRadius * 1024,
- outside: true,
- action: (xy, _) => cliffPlan[xy] = false);
+ param.Roughness, FractionMax);
+ var cliffPlan = Matrix.Zip(landPlan, mapShape, (a, b) => a && b);
- for (var altitude = 1; altitude <= param.MaximumAltitude; altitude++)
+ for (var altitude = 0; altitude < param.MaximumAltitude; altitude++)
{
- // Limit mountain area to the existing mountain space (starting with all available land)
- var roominess = MatrixUtils.ChebyshevRoom(cliffPlan, true);
- var available = 0;
- var total = size.Width * size.Height;
- for (var n = 0; n < mountainElevation.Data.Length; n++)
- {
- if (roominess.Data[n] < param.MinimumTerrainContourSpacing)
- mountainElevation.Data[n] = -1;
- else
- available++;
-
- total++;
- }
-
- MatrixUtils.CalibrateQuantileInPlace(
- mountainElevation,
- 0,
- total - available * param.Mountains / FractionMax, total);
+ cliffPlan = terraformer.SliceElevation(
+ elevation,
+ cliffPlan,
+ param.Mountains,
+ param.MinimumTerrainContourSpacing);
cliffPlan = MatrixUtils.BooleanBlotch(
- mountainElevation.Map(v => v >= 0),
+ cliffPlan,
param.TerrainSmoothing,
- param.SmoothingThreshold, FractionMax,
+ param.SmoothingThreshold, /*smoothingThresholdOutOf=*/FractionMax,
param.MinimumMountainThickness,
/*bias=*/false);
var unmaskedCliffs = MatrixUtils.BordersToPoints(cliffPlan);
@@ -804,543 +618,129 @@ namespace OpenRA.Mods.Common.Traits
break;
foreach (var cliff in cliffs)
{
- var isLoop = cliff[0] == cliff[^1];
- TilingPath cliffPath;
- if (isLoop)
- cliffPath = new TilingPath(
- map,
- cliff,
- (param.MinimumMountainThickness - 1) / 2,
- param.CliffSegmentTypes[0],
- param.CliffSegmentTypes[0],
- loopedCliffPermittedTemplates);
- else
- cliffPath = new TilingPath(
- map,
- cliff,
- (param.MinimumMountainThickness - 1) / 2,
- param.ClearSegmentTypes[0],
- param.ClearSegmentTypes[0],
- nonLoopedCliffPermittedTemplates);
- cliffPath
- .ExtendEdge(4)
- .SetAutoEndDeviation()
- .OptimizeLoop();
+ var cliffPath = TilingPath.QuickCreate(
+ map,
+ param.SegmentedBrushes,
+ cliff,
+ (param.MinimumMountainThickness - 1) / 2,
+ param.CliffSegmentTypes[0],
+ param.ClearSegmentTypes[0])
+ .ExtendEdge(4);
var brush = cliffPath.Tile(cliffTilingRandom)
- ?? throw new MapGenerationException("Could not fit tiles for cliffs");
- brush.Paint(map, actorPlans, CPos.Zero, MultiBrush.Replaceability.Tile, pickAnyRandom);
+ ?? throw new MapGenerationException("Could not fit tiles for cliffs");
+ terraformer.PaintTiling(pickAnyRandom, brush);
}
}
}
if (param.Forests > 0)
{
- var forestNoise = new CellLayer