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(map); - NoiseUtils.SymmetricFractalNoiseIntoCellLayer( + var space = terraformer.CheckSpace(param.ClearTerrain); + var passages = terraformer.PlanPassages( + topologyRandom, + terraformer.ImproveSymmetry(space, true, (a, b) => a && b), + param.ForestCutout, + param.MaximumCutoutSpacing); + var forestNoise = terraformer.BooleanNoise( forestRandom, - forestNoise, - param.Rotations, - param.Mirror, param.ForestFeatureSize, - wavelength => ClumpinessAmplitude(wavelength, param.ForestClumpiness)); - CellLayerUtils.CalibrateQuantileInPlace( - forestNoise, - 0, - FractionMax - param.Forests, FractionMax); - - var forestPlan = new CellLayer(map); + param.Forests, + param.ForestClumpiness); + var replace = PlayableToReplaceable(); foreach (var mpos in map.AllCells.MapCoords) - if (param.ClearTerrain.Contains(map.GetTerrainIndex(mpos)) && forestNoise[mpos] >= 0) - forestPlan[mpos] = true; - - if (param.ForestCutout > 0) - { - var space = new CellLayer(map); - foreach (var mpos in map.AllCells.MapCoords) - space[mpos] = param.ClearTerrain.Contains(map.GetTerrainIndex(mpos)); - - // Improve symmetry. - { - var newSpace = new CellLayer(map); - Symmetry.RotateAndMirrorOverCPos( - space, - param.Rotations, - param.Mirror, - (sources, destination) - => newSpace[destination] = - sources.All(source => !space.TryGetValue(source, out var value) || value)); - space = newSpace; - } - - if (param.MaximumCutoutSpacing > 0) - { - var roominess = new CellLayer(map); - CellLayerUtils.ChebyshevRoom(roominess, space, false); - foreach (var mpos in map.AllCells.MapCoords) - roominess[mpos] = Math.Min( - param.MaximumCutoutSpacing, - roominess[mpos]); - - while (true) - { - var (chosenMPos, room) = CellLayerUtils.FindRandomBest( - roominess, - topologyRandom, - (a, b) => a.CompareTo(b)); - if (room < param.MaximumCutoutSpacing) - break; - - var projections = Symmetry.RotateAndMirrorCPos( - chosenMPos.ToCPos(map), - space, - param.Rotations, - param.Mirror); - foreach (var projection in projections) - { - if (space.Contains(projection)) - space[projection] = false; - var minX = projection.X - 2 * param.MaximumCutoutSpacing + 1; - var minY = projection.Y - 2 * param.MaximumCutoutSpacing + 1; - var maxX = projection.X + 2 * param.MaximumCutoutSpacing - 1; - var maxY = projection.Y + 2 * param.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 * param.ForestCutout, 2 * param.ForestCutout).Fill(true); - var inflated = MatrixUtils.KernelDilateOrErode(deflated.Map(v => v != 0), kernel, new int2(param.ForestCutout - 1, param.ForestCutout - 1), true); - var cutout = new CellLayer(map); - CellLayerUtils.FromMatrix(cutout, inflated, true); - foreach (var mpos in map.AllCells.MapCoords) - if (cutout[mpos]) - forestPlan[mpos] = false; - } - - var replaceable = IdentifyReplaceableTiles(map, replaceabilityMap, null); - var forestReplace = new CellLayer(map); - foreach (var mpos in map.AllCells.MapCoords) - if (forestPlan[mpos]) - forestReplace[mpos] = replaceable[mpos]; - else - forestReplace[mpos] = MultiBrush.Replaceability.None; - MultiBrush.PaintArea(map, actorPlans, forestReplace, param.ForestObstacles, forestTilingRandom); + if (!forestNoise[mpos] || !space[mpos] || passages[mpos]) + replace[mpos] = MultiBrush.Replaceability.None; + terraformer.PaintArea(forestTilingRandom, replace, param.ForestObstacles); } if (param.EnforceSymmetry != 0) { - // This is not commutative. - bool CheckCompatibility(byte main, byte other) - { - if (main == other) - return true; - else if (param.DominantTerrain.Contains(main)) - return true; - else if (param.DominantTerrain.Contains(other)) - return false; - else - return param.EnforceSymmetry < 2; - } - - var replace = new CellLayer(map); - Symmetry.RotateAndMirrorOverCPos( - replace, - param.Rotations, - param.Mirror, - (CPos[] sources, CPos destination) => - { - var main = templatedTerrainInfo.GetTerrainIndex(map.Tiles[destination]); - var compatible = sources - .Where(replace.Contains) - .Select(source => templatedTerrainInfo.GetTerrainIndex(map.Tiles[source])) - .All(source => CheckCompatibility(main, source)); - replace[destination] = compatible ? MultiBrush.Replaceability.None : MultiBrush.Replaceability.Actor; - }); - MultiBrush.PaintArea(map, actorPlans, replace, param.ForestObstacles, symmetryTilingRandom); + var asymmetries = terraformer.FindAsymmetries(param.DominantTerrain, true, param.EnforceSymmetry == 2); + terraformer.PaintActors(symmetryTilingRandom, asymmetries, param.ForestObstacles); } - var playableArea = new CellLayer(map); + CellLayer playable; { - var (regions, regionMask, playability) = PlayableSpace.FindPlayableRegions(map, actorPlans, playabilityMap); - PlayableSpace.Region largest = null; - var disqualifications = new HashSet(); - // For circle-in-mountains, the outside is unplayable and should never count as // the largest/preferred region. + CellLayer poison = null; if (param.ExternalCircularBias > 0) - { - if (map.Grid.Type != MapGridType.Rectangular) - throw new NotImplementedException(); - CellLayerUtils.OverCircle( - cellLayer: regionMask, - wCenter: wMapCenter, - wRadius: (minSpan - 2) * 512, - outside: true, - action: (mpos, _, _, _) => - { - if (regionMask[mpos] != PlayableSpace.NullRegion) - disqualifications.Add(regionMask[mpos]); - }); - } + poison = terraformer.CenteredCircle( + false, true, CellLayerUtils.Radius(map.Tiles) - new WDist(1024)); - // Disqualify regions that violate any symmetry requirements. - { - var symmetryScore = new int[regions.Length]; - void TestSymmetry(CPos[] sources, CPos destination) - { - var id = regionMask[destination]; - if (playability[destination] != PlayableSpace.Playability.Playable) - return; - if (sources.All(source => regionMask.TryGetValue(source, out var sourceId) && sourceId == id)) - symmetryScore[id]++; - } - - Symmetry.RotateAndMirrorOverCPos( - regionMask, - param.Rotations, - param.Mirror, - TestSymmetry); - - for (var id = 0; id < symmetryScore.Length; id++) - if (symmetryScore[id] < regions[id].PlayableArea / 2) - disqualifications.Add(id); - } - - foreach (var region in regions) - { - if (disqualifications.Contains(region.Id)) - continue; - if (largest == null || region.PlayableArea > largest.PlayableArea) - largest = region; - } - - if (largest == null) - throw new MapGenerationException("could not find a playable region"); + playable = terraformer.ChoosePlayableRegion( + terraformer.CheckSpace(param.PlayableTerrain, true, false, true), + poison) + ?? throw new MapGenerationException("could not find a playable region"); var minimumPlayableSpace = (int)(param.Players * Math.PI * param.SpawnBuildSize * param.SpawnBuildSize); - if (largest.PlayableArea < minimumPlayableSpace) + if (playable.Count(p => p) < minimumPlayableSpace) throw new MapGenerationException("playable space is too small"); - bool? AdoptPartiallyPlayableIntoLargest(CPos cpos, bool first) - { - if (first) - return false; - else if (regionMask[cpos] == largest.Id || playability[cpos] != PlayableSpace.Playability.Partial) - return null; - - regionMask[cpos] = largest.Id; - largest.Area++; - return false; - } - - // Adopt any partially playable tiles connected to the largest region into the largest region. - // This avoids potentially debris-filling connected oceans for mods where water is unplayable. - CellLayerUtils.FloodFill( - regionMask, - map.AllCells.Where(cpos => regionMask[cpos] == largest.Id).Select(cpos => (cpos, true)), - AdoptPartiallyPlayableIntoLargest, - DirectionExts.Spread4CVec); - if (param.DenyWalledAreas) { // Beach tiles are particularly problematic. If they're for unplayable bodies // of water, they should be obliterated. If they're just surrounded by rocks, // trees, etc, they should be filled in with actors. + if (waterIsPlayable) { - var unplayableWater = new HashSet(); - foreach (var mpos in map.AllCells.MapCoords) - if (map.Contains(mpos) && - map.Tiles[mpos].Type == param.WaterTile && - regionMask[mpos] != largest.Id) - { - var cpos = mpos.ToCPos(gridType); - var projections = Symmetry.RotateAndMirrorCPos( - cpos, map.Tiles, param.Rotations, param.Mirror); - foreach (var projection in projections) - if (map.Tiles.Contains(projection) && map.Tiles[projection].Type == param.WaterTile) - unplayableWater.Add(projection); - } - - bool? ClearWaterBody(CPos cpos, bool _) - { - var mpos = cpos.ToMPos(gridType); - var propagate = - beachesShape.Remove(cpos) || - map.Tiles[mpos].Type == param.WaterTile; - map.Tiles[mpos] = PickTile(param.LandTile); - regionMask[mpos] = PlayableSpace.NullRegion; - return propagate ? false : null; - } - - CellLayerUtils.FloodFill( - map.Tiles, - unplayableWater.Select(cpos => (cpos, false)), - ClearWaterBody, - DirectionExts.Spread4CVec); + var mask = CellLayerUtils.Clone(playable); + terraformer.ZoneFromOutOfBounds(mask, true); + terraformer.FillUnmaskedSideAndBorder( + mask, + landBeachWater, + Terraformer.Side.Out, + cpos => map.Tiles[cpos] = terraformer.PickTile(pickAnyRandom, param.LandTile)); } - var replaceable = IdentifyReplaceableTiles(map, replaceabilityMap, actorPlans); - var replace = new CellLayer(map); + var replace = PlayableToReplaceable(); foreach (var mpos in map.AllCells.MapCoords) - if (regionMask[mpos] == largest.Id || !map.Contains(mpos)) + if (playable[mpos] || !map.Contains(mpos)) replace[mpos] = MultiBrush.Replaceability.None; - else - replace[mpos] = replaceable[mpos]; - MultiBrush.PaintArea(map, actorPlans, replace, param.UnplayableObstacles, debrisTilingRandom); + terraformer.PaintArea(debrisTilingRandom, replace, param.UnplayableObstacles); } - - foreach (var mpos in map.AllCells.MapCoords) - playableArea[mpos] = playability[mpos] == PlayableSpace.Playability.Playable && regionMask[mpos] == largest.Id; } if (param.Roads) { - // For awkward symmetries, we try harder to make sure roads are fairer. - // This can degrade the quantity of roads, though. - var imperfectSymmetry = - param.Mirror != Symmetry.Mirror.None || - param.Rotations == 3 || - param.Rotations >= 5; - - // 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] = param.ClearTerrain.Contains(templatedTerrainInfo.GetTerrainIndex(map.Tiles[cpos])); - - foreach (var actorPlan in actorPlans) - foreach (var (cpos, _) in actorPlan.Footprint()) - if (space.Contains(cpos + enlargedOffset)) - space[cpos + enlargedOffset] = false; - - // Improve symmetry. - { - var newSpace = new CellLayer(gridType, enlargedSize); - Symmetry.RotateAndMirrorOverCPos( - space, - param.Rotations, - param.Mirror, - (sources, destination) - => newSpace[destination] = - sources.All(source => !space.TryGetValue(source, out var value) || value)); - space = newSpace; - } - - // TODO: Move to configuration + // TODO: Move or collapse into configuration + const int RoadMinimumShrinkLength = 12; const int RoadStraightenShrink = 4; const int RoadStraightenGrow = 2; - const int RoadMinimumShrinkLength = 12; const int RoadInertialRange = 8; - var roadTotalShrink = RoadStraightenShrink + param.RoadShrink; - var minimumRoadLengthForPruning = RoadMinimumShrinkLength + 2 * roadTotalShrink; - var matrixSpace = CellLayerUtils.ToMatrix(space, true); - var kernel = new Matrix(param.RoadSpacing * 2 + 1, param.RoadSpacing * 2 + 1); - MatrixUtils.OverCircle( - matrix: kernel, - centerIn1024ths: kernel.Size * 512, - radiusIn1024ths: param.RoadSpacing * 1024, - outside: false, - action: (xy, _) => kernel[xy] = true); - var dilated = MatrixUtils.KernelDilateOrErode( - matrixSpace, - kernel, - new int2(param.RoadSpacing, param.RoadSpacing), - false); - var deflated = MatrixUtils.DeflateSpace(dilated, true); - - if (imperfectSymmetry) + var roadPaths = terraformer.PlanRoads( + terraformer.CheckSpace(param.ClearTerrain, true, false), + param.RoadSpacing, + RoadMinimumShrinkLength + 2 * (RoadStraightenShrink + param.RoadShrink)); + foreach (var roadPath in roadPaths) { - 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 < minimumRoadLengthForPruning) - { - 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, param.Rotations, param.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: minimumRoadLengthForPruning, - minimumJunctionSeparation: 6, - preserveEdgePaths: true); - var pointArrays = CellLayerUtils.FromMatrixPoints(matrixPointArrays, space); - pointArrays = TilingPath.RetainDisjointPaths(pointArrays); - pointArrays = pointArrays.Select(a => a.Select(p => p - enlargedOffset).ToArray()).ToArray(); - - var nonLoopedRoadPermittedTemplates = - TilingPath.PermittedSegments.FromInnerAndTerminalTypes( - param.SegmentedBrushes, param.RoadSegmentTypes, param.ClearSegmentTypes); - var loopedRoadPermittedTemplates = - TilingPath.PermittedSegments.FromType( - param.SegmentedBrushes, param.RoadSegmentTypes); - - foreach (var pointArray in pointArrays) - { - var isLoop = pointArray[0] == pointArray[^1]; - TilingPath path; - if (isLoop) - path = new TilingPath( - map, - pointArray, - param.RoadSpacing - 1, - param.RoadSegmentTypes[0], - param.RoadSegmentTypes[0], - loopedRoadPermittedTemplates); - else - path = new TilingPath( - map, - pointArray, - param.RoadSpacing - 1, - param.ClearSegmentTypes[0], - param.ClearSegmentTypes[0], - nonLoopedRoadPermittedTemplates); - - path - .ChirallyNormalize(cvec => CellLayerUtils.CornerToWPos(cvec, gridType) - wMapCenter) - .ExtendEdge(2 * roadTotalShrink + RoadMinimumShrinkLength) - .Shrink(roadTotalShrink, RoadMinimumShrinkLength) - .InertiallyExtend(RoadStraightenGrow, RoadInertialRange) - .SetAutoEndDeviation() - .OptimizeLoop() - .RetainIfValid(); - - // Shrinking may have deleted the path. - if (path.Points == null) + var tilingPath = TilingPath.QuickCreate( + map, + param.SegmentedBrushes, + roadPath, + param.RoadSpacing - 1, + param.RoadSegmentTypes[0], + param.ClearSegmentTypes[0]) + .StraightenEnds( + RoadStraightenShrink + param.RoadShrink, + RoadStraightenGrow, + RoadMinimumShrinkLength, + RoadInertialRange) + .RetainIfValid(); + if (tilingPath.Points == null) continue; - var brush = path.Tile(roadTilingRandom) + var brush = tilingPath.Tile(roadTilingRandom) ?? throw new MapGenerationException("Could not fit tiles for roads"); - brush.Paint(map, actorPlans, CPos.Zero, MultiBrush.Replaceability.Tile, pickAnyRandom); + terraformer.PaintTiling(pickAnyRandom, brush); } } if (param.CreateEntities) { - var (buildingTypes, buildingWeights) = Parameters.SplitWeights(param.BuildingWeights); - var (resourceSpawnTypes, resourceSpawnWeights) = Parameters.SplitWeights(param.ResourceSpawnWeights); - - var projectionSpacing = new CellLayer(map); - Symmetry.RotateAndMirrorOverCPos( - projectionSpacing, - param.Rotations, - param.Mirror, - (projections, cpos) => - projectionSpacing[cpos] = Symmetry.ProjectionProximity(projections) / 2); - - // Spawn bias tries to move spawns away from the map center and their symmetry - // projections. - var spawnBiasRadius = Math.Max(1, minSpan * param.CentralSpawnReservationFraction / FractionMax); - var spawnBias = new CellLayer(map); - spawnBias.Clear(spawnBiasRadius); - CellLayerUtils.OverCircle( - cellLayer: spawnBias, - wCenter: wMapCenter, - wRadius: 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]); - - var zoneable = new CellLayer(map); - foreach (var mpos in map.AllCells.MapCoords) - zoneable[mpos] = playableArea[mpos] && param.ZoneableTerrain.Contains(templatedTerrainInfo.GetTerrainIndex(map.Tiles[mpos])); - - foreach (var actorPlan in actorPlans) - foreach (var cpos in actorPlan.Footprint().Keys) - if (map.AllCells.Contains(cpos)) - zoneable[cpos] = false; - - // Improve symmetry. - { - var newZoneable = new CellLayer(map); - Symmetry.RotateAndMirrorOverCPos( - zoneable, - param.Rotations, - param.Mirror, - (sources, destination) - => newZoneable[destination] = - sources.All(source => zoneable.TryGetValue(source, out var value) && value)); - zoneable = newZoneable; - } - - if (param.Rotations > 1 || param.Mirror != Symmetry.Mirror.None) - { - // Reserve the center of the map - otherwise it will mess with rotations - CellLayerUtils.OverCircle( - cellLayer: zoneable, - wCenter: wMapCenter, - wRadius: 1024, - outside: false, - action: (mpos, _, _, _) => zoneable[mpos] = false); - } + var zoneable = terraformer.GetZoneable(param.ZoneableTerrain, playable); var zoneableArea = zoneable.Count(v => v); var symmetryCount = Symmetry.RotateAndMirrorProjectionCount(param.Rotations, param.Mirror); @@ -1353,90 +753,36 @@ namespace OpenRA.Mods.Common.Traits var symmetryPlayers = param.Players / symmetryCount; for (var iteration = 0; iteration < symmetryPlayers; iteration++) { - var spawnPreference = new CellLayer(map); - CellLayerUtils.ChebyshevRoom(spawnPreference, zoneable, false); - foreach (var mpos in map.AllCells.MapCoords) - if (spawnPreference[mpos] >= param.MinimumSpawnRadius && - projectionSpacing[mpos] * 2 >= param.SpawnReservation + param.MinimumSpawnRadius) - { - spawnPreference[mpos] = spawnBias[mpos] * Math.Min(param.SpawnRegionSize, spawnPreference[mpos]); - } - else - { - spawnPreference[mpos] = 0; - } - - var (chosenMPos, chosenValue) = CellLayerUtils.FindRandomBest( - spawnPreference, + var chosenCPos = terraformer.ChooseSpawnInZoneable( playerRandom, - (a, b) => a.CompareTo(b)); - - if (chosenValue < 1) - throw new MapGenerationException("Not enough room for player spawns"); + zoneable, + param.CentralSpawnReservationFraction, + param.MinimumSpawnRadius, + param.SpawnRegionSize, + param.SpawnReservation) + ?? throw new MapGenerationException("Not enough room for player spawns"); var spawn = new ActorPlan(map, "mpspawn") { - Location = chosenMPos.ToCPos(gridType), + Location = chosenCPos, }; - var preferedRange1024ths = (param.SpawnBuildSize + param.SpawnRegionSize * 2) * 512; - var resourceSpawnPreferences = new CellLayer(map); - CellLayerUtils.WalkingDistances( - resourceSpawnPreferences, + var resourceSpawnPreferences = terraformer.TargetWalkingDistance( + terraformer.CheckSpace(param.PlayableTerrain, true), + terraformer.ErodeZones(zoneable, 1), + [chosenCPos], + new WDist((param.SpawnBuildSize + param.SpawnRegionSize * 2) * 512), + new WDist(param.SpawnRegionSize * 1024)); + terraformer.AddDistributedActors( + playerRandom, zoneable, - [chosenMPos.ToCPos(gridType)], - param.SpawnRegionSize * 1024); - foreach (var mpos in map.AllCells.MapCoords) - { - var v = resourceSpawnPreferences[mpos]; - resourceSpawnPreferences[mpos] = - ((v > preferedRange1024ths ? 2 * preferedRange1024ths - v : v) + 1023) / 1024; - } + resourceSpawnPreferences, + param.ResourceSpawnWeights, + param.SpawnResourceSpawns, + false, + new WDist(param.ResourceSpawnReservation * 1024)); - var resourceSpawns = new List(); - for (var resourceSpawn = 0; resourceSpawn < param.SpawnResourceSpawns; resourceSpawn++) - { - var (mpos, value) = CellLayerUtils.FindRandomBest( - resourceSpawnPreferences, - playerRandom, - (a, b) => a.CompareTo(b)); - if (value <= 1) - break; - - var resourceSpawnType = resourceSpawnTypes[playerRandom.PickWeighted(resourceSpawnWeights)]; - var resourceSpawnPlan = - new ActorPlan(map, resourceSpawnType) - { - Location = mpos.ToCPos(gridType) - }; - resourceSpawns.Add(resourceSpawnPlan); - CellLayerUtils.OverCircle( - cellLayer: resourceSpawnPreferences, - wCenter: resourceSpawnPlan.WPosLocation, - wRadius: 1024, - outside: false, - action: (mpos, _, _, _) => resourceSpawnPreferences[mpos] = 0); - } - - var projectedSpawns = Symmetry.RotateAndMirrorActorPlan(spawn, param.Rotations, param.Mirror); - actorPlans.AddRange(projectedSpawns); - foreach (var projectedSpawn in projectedSpawns) - CellLayerUtils.OverCircle( - cellLayer: zoneable, - wCenter: projectedSpawn.WPosLocation, - wRadius: param.SpawnReservation * 1024, - outside: false, - action: (mpos, _, _, _) => zoneable[mpos] = false); - - var projectedResourceSpawns = Symmetry.RotateAndMirrorActorPlans(resourceSpawns, param.Rotations, param.Mirror); - actorPlans.AddRange(projectedResourceSpawns); - foreach (var projectedResourceSpawn in projectedResourceSpawns) - CellLayerUtils.OverCircle( - cellLayer: zoneable, - wCenter: projectedResourceSpawn.WPosLocation, - wRadius: param.ResourceSpawnReservation * 1024, - outside: false, - action: (mpos, _, _, _) => zoneable[mpos] = false); + terraformer.ProjectPlaceDezoneActor(spawn, zoneable, new WDist(param.SpawnReservation * 1024)); } // Expansions @@ -1444,480 +790,115 @@ namespace OpenRA.Mods.Common.Traits var resourceSpawnsRemaining = (int)(param.MaximumExpansionResourceSpawns * perSymmetryEntityMultiplier / EntityBonusMax); while (resourceSpawnsRemaining > 0) { - var roominess = new CellLayer(map); - CellLayerUtils.ChebyshevRoom(roominess, zoneable, false); - foreach (var mpos in map.AllCells.MapCoords) - roominess[mpos] = Math.Min( - param.MaximumExpansionSize + param.ExpansionBorder, - Math.Min(roominess[mpos], projectionSpacing[mpos])); - var (chosenMPos, chosenValue) = CellLayerUtils.FindRandomBest( - roominess, + var added = terraformer.AddActorCluster( expansionRandom, - (a, b) => a.CompareTo(b)); - var room = chosenValue - 1; - var radius2 = room - param.ExpansionBorder; - if (radius2 < param.MinimumExpansionSize) + zoneable, + param.ResourceSpawnWeights, + Math.Min(resourceSpawnsRemaining, expansionRandom.Next(param.MaximumResourceSpawnsPerExpansion) + 1), + param.ExpansionInner, + param.MinimumExpansionSize, + param.MaximumExpansionSize, + param.ExpansionBorder, + true, + new WDist(param.ResourceSpawnReservation * 1024)); + resourceSpawnsRemaining -= added; + if (added == 0) break; - if (radius2 > param.MaximumExpansionSize) - radius2 = param.MaximumExpansionSize; - var radius1 = Math.Min(Math.Min(param.ExpansionInner, room), radius2); - var resourceSpawnCount = Math.Min(resourceSpawnsRemaining, expansionRandom.Next(param.MaximumResourceSpawnsPerExpansion) + 1); - resourceSpawnsRemaining -= resourceSpawnCount; - - if (radius1 < 1) - break; - - var resourceSpawns = new List(); - var resourceSpawnPreferences = new CellLayer(map); - var radius1Sq = radius1 * radius1; - CellLayerUtils.OverCircle( - cellLayer: resourceSpawnPreferences, - wCenter: CellLayerUtils.MPosToWPos(chosenMPos, gridType), - wRadius: radius2 * 1024, - outside: false, - action: (mpos, _, _, wrSq) => - { - var rSq = (int)(wrSq / (1024 * 1024)); - resourceSpawnPreferences[mpos] = - rSq >= radius1Sq ? rSq : 0; - }); - for (var resourceSpawn = 0; resourceSpawn < resourceSpawnCount; resourceSpawn++) - { - var mpos = CellLayerUtils.PickWeighted(resourceSpawnPreferences, expansionRandom); - var resourceSpawnType = resourceSpawnTypes[playerRandom.PickWeighted(resourceSpawnWeights)]; - var resourceSpawnPlan = - new ActorPlan(map, resourceSpawnType) - { - Location = mpos.ToCPos(gridType) - }; - resourceSpawns.Add(resourceSpawnPlan); - CellLayerUtils.OverCircle( - cellLayer: resourceSpawnPreferences, - wCenter: resourceSpawnPlan.WPosLocation, - wRadius: 1024, - outside: false, - action: (mpos, _, _, _) => resourceSpawnPreferences[mpos] = 0); - } - - var projectedResourceSpawns = Symmetry.RotateAndMirrorActorPlans(resourceSpawns, param.Rotations, param.Mirror); - actorPlans.AddRange(projectedResourceSpawns); - foreach (var projectedResourceSpawn in projectedResourceSpawns) - CellLayerUtils.OverCircle( - cellLayer: zoneable, - wCenter: projectedResourceSpawn.WPosLocation, - wRadius: param.ResourceSpawnReservation * 1024, - outside: false, - action: (mpos, _, _, _) => zoneable[mpos] = false); } } // Neutral buildings { + var (buildingTypes, buildingWeights) = Terraformer.SplitDictionary(param.BuildingWeights); var targetBuildingCount = (param.MaximumBuildings != 0) - ? expansionRandom.Next( + ? buildingRandom.Next( (int)(param.MinimumBuildings * perSymmetryEntityMultiplier / EntityBonusMax), (int)(param.MaximumBuildings * perSymmetryEntityMultiplier / EntityBonusMax) + 1) : 0; for (var i = 0; i < targetBuildingCount; i++) - { - var roominess = new CellLayer(map); - CellLayerUtils.ChebyshevRoom(roominess, zoneable, false); - foreach (var mpos in map.AllCells.MapCoords) - roominess[mpos] = Math.Min( - 3, - Math.Min(roominess[mpos], projectionSpacing[mpos])); - var (chosenMPos, chosenValue) = CellLayerUtils.FindRandomBest( - roominess, + terraformer.AddActor( buildingRandom, - (a, b) => a.CompareTo(b)); - if (chosenValue < 3) - break; - var chosenCPos = chosenMPos.ToCPos(gridType); - var typeChoice = buildingRandom.PickWeighted(buildingWeights); - var type = buildingTypes[typeChoice]; - var actorPlan = new ActorPlan(map, type) - { - WPosCenterLocation = CellLayerUtils.CPosToWPos(chosenCPos, gridType), - }; - - var projectedBuildings = Symmetry.RotateAndMirrorActorPlan(actorPlan, param.Rotations, param.Mirror); - actorPlans.AddRange(projectedBuildings); - foreach (var projectedBuilding in projectedBuildings) - CellLayerUtils.OverCircle( - cellLayer: zoneable, - wCenter: projectedBuilding.WPosLocation, - wRadius: 2048, - outside: false, - action: (mpos, _, _, _) => zoneable[mpos] = false); - } + zoneable, + buildingTypes[buildingRandom.PickWeighted(buildingWeights)]); } // Grow resources + var targetResourceValue = param.ResourcesPerPlayer * entityMultiplier / EntityBonusMax; + if (targetResourceValue > 0) { - var pattern1024ths = new CellLayer(map); - NoiseUtils.SymmetricFractalNoiseIntoCellLayer( + var resourcePattern = terraformer.ResourceNoise( resourceRandom, - pattern1024ths, - param.Rotations, - param.Mirror, param.ResourceFeatureSize, - wavelength => ClumpinessAmplitude(wavelength, param.OreClumpiness)); - { - CellLayerUtils.CalibrateQuantileInPlace( - pattern1024ths, - 0, - 0, 1); - var max1024ths = pattern1024ths.Max(); - var uniformity1024ths = param.OreUniformity * 1024 / FractionMax; - foreach (var mpos in map.AllCells.MapCoords) - pattern1024ths[mpos] = uniformity1024ths + 1024 * pattern1024ths[mpos] / max1024ths; - } - - var strengths1024ths = new Dictionary>(); - foreach (var actorPlan in actorPlans) - { - var type = actorPlan.Reference.Type; - if (param.ResourceSpawnWeights.ContainsKey(type)) - { - var resource = param.ResourceSpawnSeeds[type]; - if (!strengths1024ths.TryGetValue(resource, out var strength1024ths)) - { - strength1024ths = new CellLayer(map); - strength1024ths.Clear(1); - strengths1024ths.Add(resource, strength1024ths); - } - - CellLayerUtils.OverCircle( - cellLayer: strength1024ths, - wCenter: actorPlan.WPosLocation, - wRadius: 16 * 1024, - outside: false, - action: (mpos, _, _, wrSq) => - strength1024ths[mpos] += - (int)(1024 * 1024 / (1024 + Exts.ISqrt(wrSq)))); - } - } - - var maxStrength1024ths = new CellLayer(map); - maxStrength1024ths.Clear(1); - var bestResource = new CellLayer(map); - bestResource.Clear(param.DefaultResource); - foreach (var resourceStrength in strengths1024ths) - { - 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] = pattern1024ths[mpos] * maxStrength1024ths[mpos]; + param.OreClumpiness, + param.OreUniformity * 1024 / FractionMax); + var resourceBiases = new List(); var wSpawnBuildSizeSq = (long)param.SpawnBuildSize * param.SpawnBuildSize * 1024 * 1024; - foreach (var actorPlan in actorPlans) - if (actorPlan.Reference.Type == "mpspawn") - CellLayerUtils.OverCircle( - cellLayer: plan, - wCenter: actorPlan.WPosLocation, - wRadius: param.SpawnRegionSize * 2 * 1024, - outside: false, - action: (mpos, _, _, rSq) => - plan[mpos] += (int)(plan[mpos] * param.SpawnResourceBias * wSpawnBuildSizeSq / Math.Max(rSq, 1024 * 1024) / FractionMax)); - foreach (var mpos in map.AllCells.MapCoords) - if (!playableArea[mpos] || !param.AllowedTerrainResourceCombos.Contains((bestResource[mpos], map.GetTerrainIndex(mpos)))) - plan[mpos] = -int.MaxValue; - - foreach (var actorPlan in actorPlans) - if (actorPlan.Reference.Type == "mpspawn") - CellLayerUtils.OverCircle( - cellLayer: plan, - wCenter: actorPlan.WPosLocation, - wRadius: param.SpawnBuildSize * 1024, - outside: false, - action: (mpos, _, _, _) => plan[mpos] = -int.MaxValue); - - foreach (var actorPlan in actorPlans) - foreach (var (cpos, _) in actorPlan.Footprint()) - if (plan.Contains(cpos)) - plan[cpos] = -int.MaxValue; - - // Improve symmetry. + // Bias towards resource spawns + foreach (var (actorType, resourceType) in param.ResourceSpawnSeeds.OrderBy(kv => kv.Key)) { - var newPlan = new CellLayer(map); - Symmetry.RotateAndMirrorOverCPos( - plan, - param.Rotations, - param.Mirror, - (sources, destination) - => newPlan[destination] = - sources.Min(source => plan.TryGetValue(source, out var value) ? value : -int.MaxValue)); - plan = newPlan; + resourceBiases.AddRange( + terraformer.ActorsOfType(actorType) + .Select(a => new Terraformer.ResourceBias(a) + { + BiasRadius = new WDist(16 * 1024), + Bias = (value, rSq) => value + (int)(1024 * 1024 / (1024 + Exts.ISqrt(rSq))), + ResourceType = resourceType, + })); } - var remaining = param.ResourcesPerPlayer * entityMultiplier / EntityBonusMax; + // Bias towards player spawns, but also reserve an area for base building. + resourceBiases.AddRange( + terraformer.ActorsOfType("mpspawn") + .Select(a => new Terraformer.ResourceBias(a) + { + ExclusionRadius = new WDist(param.SpawnBuildSize * 1024), + BiasRadius = new WDist(param.SpawnRegionSize * 2 * 1024), + Bias = (value, rSq) => value + (int)(value * param.SpawnResourceBias * wSpawnBuildSizeSq / Math.Max(rSq, 1024 * 1024) / FractionMax), + })); - // 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 = bestResource[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 param.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; - } - - // Set and return change in overall value. - int AddResource(CPos cpos) - { - var mpos = cpos.ToMPos(gridType); - priorities[PriorityIndex(mpos)] = int.MaxValue; - zoneable[mpos] = false; - - // Generally shouldn't happen, but perhaps a rotation/mirror related inaccuracy. - if (map.Resources[mpos].Type != 0) - return 0; - - var resourceType = bestResource[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, param.Rotations, param.Mirror)) - if (map.Resources.Contains(cpos)) - remaining -= AddResource(cpos); - } + var (plan, typePlan) = terraformer.PlanResources( + resourcePattern, + CellLayerUtils.Intersect([playable, terraformer.CheckSpace(null, true)]), + param.DefaultResource, + resourceBiases); + terraformer.GrowResources( + plan, + typePlan, + targetResourceValue); + terraformer.ZoneFromResources(zoneable, false); } // CivilianBuildings if (param.CivilianBuildings > 0) { - var space = new CellLayer(map); - foreach (var mpos in map.AllCells.MapCoords) - space[mpos] = param.PlayableTerrain.Contains(templatedTerrainInfo.GetTerrainIndex(map.Tiles[mpos])); - - foreach (var actorPlan in actorPlans) - foreach (var (cpos, _) in actorPlan.Footprint()) - if (space.Contains(cpos)) - space[cpos] = false; - - 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( + var decorationNoise = terraformer.DecorationPattern( decorationRandom, - decorationNoise, - param.Rotations, - param.Mirror, + terraformer.CheckSpace(param.PlayableTerrain, true), + CellLayerUtils.Intersect([zoneable, terraformer.CheckSpace(param.LandTile)]), + param.CivilianBuildings, param.CivilianBuildingsFeatureSize, - wavelength => 1); - - var densityNoise = new CellLayer(map); - NoiseUtils.SymmetricFractalNoiseIntoCellLayer( - decorationRandom, - densityNoise, - param.Rotations, - param.Mirror, - 1024, - NoiseUtils.PinkAmplitude); - CellLayerUtils.CalibrateQuantileInPlace( - densityNoise, - 0, - FractionMax - param.CivilianBuildingDensity, FractionMax); - - var decorable = new CellLayer(map); - var totalDecorable = 0; - foreach (var mpos in map.AllCells.MapCoords) - { - var isDecorable = - map.Tiles[mpos].Type == param.LandTile - && zoneable[mpos] && space[mpos] && !reserved[mpos] && densityNoise[mpos] >= 0; - decorable[mpos] = isDecorable; - if (isDecorable) - totalDecorable++; - else - decorationNoise[mpos] = -1024 * 1024; - } - - var mapArea = map.MapSize.Width * map.MapSize.Height; - CellLayerUtils.CalibrateQuantileInPlace( - decorationNoise, - 0, - mapArea - totalDecorable * param.CivilianBuildings / FractionMax, mapArea); - foreach (var mpos in map.AllCells.MapCoords) - if (decorationNoise[mpos] < 0) - decorable[mpos] = false; - - for (var i = 0; i < 8; i++) - { - var (blurred, changes) = MatrixUtils.BooleanBlur( - CellLayerUtils.ToMatrix(decorable, false), - param.CivilianBuildingDensityRadius, - FractionMax - param.MinimumCivilianBuildingDensity, FractionMax); - if (changes == 0) - break; - - var densityFilter = new CellLayer(map); - CellLayerUtils.FromMatrix(densityFilter, blurred); - - foreach (var mpos in map.AllCells.MapCoords) - if (!densityFilter[mpos]) - decorable[mpos] = false; - } - - // Improve symmetry. - { - var newDecorable = new CellLayer(map); - Symmetry.RotateAndMirrorOverCPos( - decorable, - param.Rotations, - param.Mirror, - (sources, destination) - => newDecorable[destination] = - sources.All(source => decorable.TryGetValue(source, out var value) && value)); - decorable = newDecorable; - } - - var replace = new CellLayer(map); - foreach (var mpos in map.AllCells.MapCoords) - replace[mpos] = decorable[mpos] ? MultiBrush.Replaceability.Actor : MultiBrush.Replaceability.None; - - MultiBrush.PaintArea( - map, - actorPlans, - replace, - param.CivilianBuildingsObstacles, + param.CivilianBuildingDensity, + param.MinimumCivilianBuildingDensity, + param.CivilianBuildingDensityRadius); + terraformer.PaintActors( decorationTilingRandom, + decorationNoise, + param.CivilianBuildingsObstacles, alwaysPreferLargerBrushes: true); } } // Cosmetically repaint tiles - foreach (var (tile, collection) in param.RepaintTiles.OrderBy(kv => kv.Key)) - { - var replace = new CellLayer(map); - foreach (var mpos in replace.CellRegion.MapCoords) - replace[mpos] = - map.Tiles[mpos].Type == tile - ? MultiBrush.Replaceability.Any - : MultiBrush.Replaceability.None; + terraformer.RepaintTiles(repaintRandom, param.RepaintTiles); - MultiBrush.PaintArea(map, actorPlans, replace, collection, repaintRandom); - } - - map.PlayerDefinitions = new MapPlayers(map.Rules, param.Players).ToMiniYaml(); - map.ActorDefinitions = actorPlans - .Select((plan, i) => new MiniYamlNode($"Actor{i}", plan.Reference.Save())) - .ToImmutableArray(); + terraformer.BakeMap(); return map; } - static CellLayer IdentifyReplaceableTiles( - Map map, - Dictionary replaceabilityMap, - IEnumerable actorPlans) - { - var output = new CellLayer(map); - - foreach (var mpos in map.AllCells.MapCoords) - { - var tile = map.Tiles[mpos]; - var replaceability = MultiBrush.Replaceability.Any; - if (replaceabilityMap.TryGetValue(tile, out var value)) - replaceability = value; - output[mpos] = replaceability; - } - - if (actorPlans != null) - foreach (var actorPlan in actorPlans) - foreach (var cpos in actorPlan.Footprint().Keys) - if (map.AllCells.Contains(cpos)) - output[cpos] = MultiBrush.Replaceability.None; - - return output; - } - - static int ClumpinessAmplitude(int wavelength, int clumpiness) - { - var amplitude = wavelength; - for (var i = 0; i < clumpiness; i++) - amplitude = Exts.ISqrt(amplitude); - return amplitude; - } - string IEditorToolInfo.Label => Name; string IEditorToolInfo.PanelWidget => PanelWidget; } diff --git a/mods/cnc/rules/map-generators.yaml b/mods/cnc/rules/map-generators.yaml index 145fb1e908..e3bcf18a6c 100644 --- a/mods/cnc/rules/map-generators.yaml +++ b/mods/cnc/rules/map-generators.yaml @@ -23,7 +23,7 @@ MaximumAltitude: 8 RoughnessRadius: 5 Roughness: 500 - MinimumTerrainContourSpacing: 6 + MinimumTerrainContourSpacing: 5 MinimumCliffLength: 10 ForestClumpiness: 1 DenyWalledAreas: True @@ -60,11 +60,8 @@ splitblue: BlueTiberium ClearTerrain: Clear PlayableTerrain: Beach,BlueTiberium,Bridge,Clear,Road,Rough,Tiberium,Wall - PartiallyPlayableTerrain: River,Tree,Water - UnplayableTerrain: Rock DominantTerrain: River,Rock,Tree,Water ZoneableTerrain: Clear,Road - PartiallyPlayableCategories: Beach,Road ClearSegmentTypes: Clear BeachSegmentTypes: Beach CliffSegmentTypes: Cliff @@ -158,16 +155,14 @@ Label: label-cnc-map-generator-choice-terrain-type-mountains Settings: Water: 0 - Mountains: 1000 + Mountains: 900 Roughness: 600 - MinimumTerrainContourSpacing: 5 Choice@MountainLakes: Label: label-cnc-map-generator-choice-terrain-type-mountain-lakes Settings: Water: 200 - Mountains: 1000 + Mountains: 900 Roughness: 850 - MinimumTerrainContourSpacing: 5 MultiChoiceOption@Shape: Label: label-cnc-map-generator-option-shape Default: Square diff --git a/mods/ra/rules/map-generators.yaml b/mods/ra/rules/map-generators.yaml index 10bb5b45f0..95a983ebd2 100644 --- a/mods/ra/rules/map-generators.yaml +++ b/mods/ra/rules/map-generators.yaml @@ -23,7 +23,7 @@ MaximumAltitude: 8 RoughnessRadius: 5 Roughness: 500 - MinimumTerrainContourSpacing: 6 + MinimumTerrainContourSpacing: 5 MinimumCliffLength: 10 ForestClumpiness: 1 DenyWalledAreas: True @@ -59,11 +59,8 @@ gmine: Gems ClearTerrain: Clear PlayableTerrain: Beach,Bridge,Clear,Gems,Ore,Road,Rough,Wall,Water - PartiallyPlayableTerrain: Tree - UnplayableTerrain: River,Rock DominantTerrain: River,Rock,Tree,Water ZoneableTerrain: Clear,Road - PartiallyPlayableCategories: Beach,Road ClearSegmentTypes: Clear BeachSegmentTypes: Beach CliffSegmentTypes: Cliff @@ -156,16 +153,14 @@ Label: label-ra-map-generator-choice-terrain-type-mountains Settings: Water: 0 - Mountains: 1000 + Mountains: 900 Roughness: 600 - MinimumTerrainContourSpacing: 5 Choice@MountainLakes: Label: label-ra-map-generator-choice-terrain-type-mountain-lakes Settings: Water: 200 - Mountains: 1000 + Mountains: 900 Roughness: 850 - MinimumTerrainContourSpacing: 5 Choice@Oceanic: Label: label-ra-map-generator-choice-terrain-type-oceanic Settings: