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