Files
OpenRA/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs
Ashley Newson 25571df2b6 Refactor ExperimentalMapGenerator into reusable methods
This change lifts large portions of ExperimentalMapGenerator's logic
into a new "Terraformer" class with highly documented methods that can
theoretically be used by alternative map generator classes. Some
additional refactoring may occur in future, subject to the practical
needs of additional map generators. ClearMapGenerator is also
simplified.

This is not a pure refactor and contains some algorithmic and
behavioral changes, as well as few minor bug fixes. Notably:

- The logic for obstructing unreachable water has been somewhat
  replaced.

- In CnC, where water is already unplayable, EMG no longer obstructs
  water. (RA still does, as water is playable there.)

- Mountain (cliff) generation is now somewhat more effective. This
  increases the number of cliffs seen on many presets. Settings should
  no longer use "Mountains: 1000".

- PlayableSpace logic has been consolidated into Terraformer.

- PlayableSpace.Playability no longer exists as PartiallyPlayable
  became redundant. Its uses have been replaced with a simple
  boolean. Consequently, some defunct code and configuration has been
  removed.

- Adjust MultiBrush replaceability contract painting behavior to be
  more intuitive.

- Fix off-by-one error in map bounds computation.

- Fix some usages of mixed up MersenneTwisters.
2025-07-14 20:09:10 +03:00

908 lines
35 KiB
C#

#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.MapGenerator;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Support;
using OpenRA.Traits;
using static OpenRA.Mods.Common.Traits.ResourceLayerInfo;
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.EditorWorld)]
public sealed class ExperimentalMapGeneratorInfo : TraitInfo<ExperimentalMapGenerator>, IEditorMapGeneratorInfo, IEditorToolInfo
{
[FieldLoader.Require]
public readonly string Type = null;
[FieldLoader.Require]
[FluentReference]
public readonly string Name = null;
[FieldLoader.Require]
[Desc("Tilesets that are compatible with this map generator.")]
public readonly string[] Tilesets = null;
[FluentReference]
[Desc("The title to use for generated maps.")]
public readonly string MapTitle = "label-random-map";
[Desc("The widget tree to open when the tool is selected.")]
public readonly string PanelWidget = "MAP_GENERATOR_TOOL_PANEL";
// This is purely of interest to the linter.
[FieldLoader.LoadUsing(nameof(FluentReferencesLoader))]
[FluentReference]
public readonly List<string> FluentReferences = null;
[FieldLoader.LoadUsing(nameof(SettingsLoader))]
public readonly MiniYaml Settings;
string IMapGeneratorInfo.Type => Type;
string IMapGeneratorInfo.Name => Name;
string IMapGeneratorInfo.MapTitle => MapTitle;
string[] IEditorMapGeneratorInfo.Tilesets => Tilesets;
static MiniYaml SettingsLoader(MiniYaml my)
{
return my.NodeWithKey("Settings").Value;
}
static List<string> FluentReferencesLoader(MiniYaml my)
{
return new MapGeneratorSettings(null, my.NodeWithKey("Settings").Value)
.Options.SelectMany(o => o.GetFluentReferences()).ToList();
}
const int FractionMax = Terraformer.FractionMax;
const int EntityBonusMax = 1000000;
sealed class Parameters
{
[FieldLoader.Require]
public readonly int Seed = default;
[FieldLoader.Require]
public readonly int Rotations = default;
[FieldLoader.LoadUsing(nameof(MirrorLoader))]
public readonly Symmetry.Mirror Mirror = default;
[FieldLoader.Require]
public readonly int Players = default;
[FieldLoader.Require]
public readonly int TerrainFeatureSize = default;
[FieldLoader.Require]
public readonly int ForestFeatureSize = default;
[FieldLoader.Require]
public readonly int ResourceFeatureSize = default;
[FieldLoader.Require]
public readonly int CivilianBuildingsFeatureSize = default;
[FieldLoader.Require]
public readonly int Water = default;
[FieldLoader.Require]
public readonly int Mountains = default;
[FieldLoader.Require]
public readonly int Forests = default;
[FieldLoader.Require]
public readonly int ForestCutout = default;
[FieldLoader.Require]
public readonly int MaximumCutoutSpacing = default;
[FieldLoader.Require]
public readonly int ExternalCircularBias = default;
[FieldLoader.Require]
public readonly int TerrainSmoothing = default;
[FieldLoader.Require]
public readonly int SmoothingThreshold = default;
[FieldLoader.Require]
public readonly int MinimumLandSeaThickness = default;
[FieldLoader.Require]
public readonly int MinimumMountainThickness = default;
[FieldLoader.Require]
public readonly int MaximumAltitude = default;
[FieldLoader.Require]
public readonly int RoughnessRadius = default;
[FieldLoader.Require]
public readonly int Roughness = default;
[FieldLoader.Require]
public readonly int MinimumTerrainContourSpacing = default;
[FieldLoader.Require]
public readonly int MinimumCliffLength = default;
[FieldLoader.Require]
public readonly int ForestClumpiness = default;
[FieldLoader.Require]
public readonly bool DenyWalledAreas = default;
[FieldLoader.Require]
public readonly int EnforceSymmetry = default;
[FieldLoader.Require]
public readonly bool Roads = default;
[FieldLoader.Require]
public readonly int RoadSpacing = default;
[FieldLoader.Require]
public readonly int RoadShrink = default;
[FieldLoader.Require]
public readonly bool CreateEntities = default;
[FieldLoader.Require]
public readonly int AreaEntityBonus = default;
[FieldLoader.Require]
public readonly int PlayerCountEntityBonus = default;
[FieldLoader.Require]
public readonly int CentralSpawnReservationFraction = default;
[FieldLoader.Require]
public readonly int ResourceSpawnReservation = default;
[FieldLoader.Require]
public readonly int SpawnRegionSize = default;
[FieldLoader.Require]
public readonly int SpawnBuildSize = default;
[FieldLoader.Require]
public readonly int MinimumSpawnRadius = default;
[FieldLoader.Require]
public readonly int SpawnResourceSpawns = default;
[FieldLoader.Require]
public readonly int SpawnReservation = default;
[FieldLoader.Require]
public readonly int SpawnResourceBias = default;
[FieldLoader.Require]
public readonly int ResourcesPerPlayer = default;
[FieldLoader.Require]
public readonly int OreUniformity = default;
[FieldLoader.Require]
public readonly int OreClumpiness = default;
[FieldLoader.Require]
public readonly int MaximumExpansionResourceSpawns = default;
[FieldLoader.Require]
public readonly int MaximumResourceSpawnsPerExpansion = default;
[FieldLoader.Require]
public readonly int MinimumExpansionSize = default;
[FieldLoader.Require]
public readonly int MaximumExpansionSize = default;
[FieldLoader.Require]
public readonly int ExpansionInner = default;
[FieldLoader.Require]
public readonly int ExpansionBorder = default;
[FieldLoader.Require]
public readonly int MinimumBuildings = default;
[FieldLoader.Require]
public readonly int MaximumBuildings = default;
[FieldLoader.LoadUsing(nameof(BuildingWeightsLoader))]
public readonly IReadOnlyDictionary<string, int> BuildingWeights = default;
[FieldLoader.Require]
public readonly int CivilianBuildings = default;
[FieldLoader.Require]
public readonly int CivilianBuildingDensity = default;
[FieldLoader.Require]
public readonly int MinimumCivilianBuildingDensity = default;
[FieldLoader.Require]
public readonly int CivilianBuildingDensityRadius = default;
[FieldLoader.Require]
public readonly ushort LandTile = default;
[FieldLoader.Require]
public readonly ushort WaterTile = default;
[FieldLoader.Ignore]
public readonly IReadOnlyList<MultiBrush> SegmentedBrushes;
[FieldLoader.Ignore]
public readonly IReadOnlyList<MultiBrush> ForestObstacles;
[FieldLoader.Ignore]
public readonly IReadOnlyList<MultiBrush> UnplayableObstacles;
[FieldLoader.Ignore]
public readonly IReadOnlyList<MultiBrush> CivilianBuildingsObstacles;
[FieldLoader.Ignore]
public readonly IReadOnlyDictionary<ushort, IReadOnlyList<MultiBrush>> RepaintTiles;
[FieldLoader.Ignore]
public readonly ResourceTypeInfo DefaultResource;
[FieldLoader.Ignore]
public readonly IReadOnlyDictionary<string, ResourceTypeInfo> ResourceSpawnSeeds;
[FieldLoader.LoadUsing(nameof(ResourceSpawnWeightsLoader))]
public readonly IReadOnlyDictionary<string, int> ResourceSpawnWeights = default;
[FieldLoader.Ignore]
public readonly IReadOnlySet<byte> ClearTerrain;
[FieldLoader.Ignore]
public readonly IReadOnlySet<byte> PlayableTerrain;
[FieldLoader.Ignore]
public readonly IReadOnlySet<byte> DominantTerrain;
[FieldLoader.Ignore]
public readonly IReadOnlySet<byte> ZoneableTerrain;
[FieldLoader.Ignore]
public readonly IReadOnlyList<string> ClearSegmentTypes;
[FieldLoader.Ignore]
public readonly IReadOnlyList<string> BeachSegmentTypes;
[FieldLoader.Ignore]
public readonly IReadOnlyList<string> CliffSegmentTypes;
[FieldLoader.Ignore]
public readonly IReadOnlyList<string> RoadSegmentTypes;
public Parameters(Map map, MiniYaml my)
{
FieldLoader.Load(this, my);
var terrainInfo = (ITemplatedTerrainInfo)map.Rules.TerrainInfo;
SegmentedBrushes = MultiBrush.LoadCollection(map, "Segmented");
ForestObstacles = MultiBrush.LoadCollection(map, my.NodeWithKey("ForestObstacles").Value.Value);
UnplayableObstacles = MultiBrush.LoadCollection(map, my.NodeWithKey("UnplayableObstacles").Value.Value);
CivilianBuildingsObstacles = MultiBrush.LoadCollection(map, my.NodeWithKey("CivilianBuildingsObstacles").Value.Value);
RepaintTiles = my.NodeWithKeyOrDefault("RepaintTiles")?.Value.ToDictionary(
k =>
{
if (Exts.TryParseUshortInvariant(k, out var tile))
return tile;
else
throw new YamlException($"RepaintTile {k} is not a ushort");
},
v => MultiBrush.LoadCollection(map, v.Value) as IReadOnlyList<MultiBrush>);
RepaintTiles ??= ImmutableDictionary<ushort, IReadOnlyList<MultiBrush>>.Empty;
var resourceTypes = map.Rules.Actors[SystemActors.World].TraitInfoOrDefault<ResourceLayerInfo>().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<PlayerResourcesInfo>();
try
{
ResourceSpawnSeeds = my.NodeWithKey("ResourceSpawnSeeds").Value
.ToDictionary(subMy => subMy.Value)
.ToDictionary(kv => kv.Key, kv => resourceTypes[kv.Value]);
}
catch (KeyNotFoundException e)
{
throw new YamlException("Bad ResourceSpawnSeeds resource: " + e);
}
switch (Rotations)
{
case 1:
case 2:
case 4:
break;
default:
EnforceSymmetry = 0;
break;
}
IReadOnlySet<byte> ParseTerrainIndexes(string key)
{
return my.NodeWithKey(key).Value.Value
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(terrainInfo.GetTerrainIndex)
.ToImmutableHashSet();
}
IReadOnlyList<string> ParseSegmentTypes(string key)
{
return my.NodeWithKey(key).Value.Value
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.ToImmutableArray();
}
ClearTerrain = ParseTerrainIndexes("ClearTerrain");
PlayableTerrain = ParseTerrainIndexes("PlayableTerrain");
DominantTerrain = ParseTerrainIndexes("DominantTerrain");
ZoneableTerrain = ParseTerrainIndexes("ZoneableTerrain");
ClearSegmentTypes = ParseSegmentTypes("ClearSegmentTypes");
BeachSegmentTypes = ParseSegmentTypes("BeachSegmentTypes");
CliffSegmentTypes = ParseSegmentTypes("CliffSegmentTypes");
RoadSegmentTypes = ParseSegmentTypes("RoadSegmentTypes");
Validate(terrainInfo);
}
static object MirrorLoader(MiniYaml my)
{
if (Symmetry.TryParseMirror(my.NodeWithKey("Mirror").Value.Value, out var mirror))
return mirror;
else
throw new YamlException($"Invalid Mirror value `{my.NodeWithKey("Mirror").Value.Value}`");
}
static IReadOnlyDictionary<string, int> BuildingWeightsLoader(MiniYaml my)
{
return my.NodeWithKey("BuildingWeights").Value.ToDictionary(subMy =>
{
if (Exts.TryParseInt32Invariant(subMy.Value, out var f))
return f;
else
throw new YamlException($"Invalid building weight `{subMy.Value}`");
});
}
static IReadOnlyDictionary<string, int> ResourceSpawnWeightsLoader(MiniYaml my)
{
return my.NodeWithKey("ResourceSpawnWeights").Value.ToDictionary(subMy =>
{
if (Exts.TryParseInt32Invariant(subMy.Value, out var f))
return f;
else
throw new YamlException($"Invalid resource spawn weight `{subMy.Value}`");
});
}
public void Validate(ITemplatedTerrainInfo terrainInfo)
{
if (Rotations < 1)
throw new MapGenerationException("Rotations must be >= 1");
if (TerrainFeatureSize < 1)
throw new MapGenerationException("TerrainFeatureSize must be >= 1");
if (ForestFeatureSize < 1)
throw new MapGenerationException("ForestFeatureSize must be >= 1");
if (ResourceFeatureSize < 1)
throw new MapGenerationException("ResourceFeatureSize must be >= 1");
if (CivilianBuildingsFeatureSize < 1)
throw new MapGenerationException("CivilianBuildingsFeatureSize must be >= 1");
if (TerrainSmoothing < 0 || TerrainSmoothing > MatrixUtils.MaxBinomialKernelRadius)
throw new MapGenerationException($"TerrainSmoothing must be between 0 and {MatrixUtils.MaxBinomialKernelRadius} inclusive");
if (SmoothingThreshold < (FractionMax + 1) / 2 || SmoothingThreshold > FractionMax)
throw new MapGenerationException($"SmoothingThreshold must be between {(FractionMax + 1) / 2} and {FractionMax} inclusive");
if (MinimumLandSeaThickness < 1)
throw new MapGenerationException("MinimumLandSeaThickness must be >= 1");
if (MinimumMountainThickness < 1)
throw new MapGenerationException("MinimumMountainThickness must be >= 1");
if (Water < 0 || Water > FractionMax)
throw new MapGenerationException($"Water must be between 0 and {FractionMax} inclusive");
if (Forests < 0 || Forests > FractionMax)
throw new MapGenerationException($"Forest must be between 0 and {FractionMax} inclusive");
if (ForestCutout < 0)
throw new MapGenerationException("ForestCutout must be >= 0");
if (MaximumCutoutSpacing < 0)
throw new MapGenerationException("TopologyAugmentationThreshold must be >= 0");
if (ForestClumpiness < 0)
throw new MapGenerationException("ForestClumpiness must be >= 0");
if (Mountains < 0 || Mountains > FractionMax)
throw new MapGenerationException($"Mountains must be between 0 and {FractionMax} inclusive");
if (Roughness < 0 || Roughness > FractionMax)
throw new MapGenerationException("Roughness must be between 0 and {FractionMax}");
if (RoughnessRadius < 1)
throw new MapGenerationException("RoughnessRadius must be >= 1");
if (MaximumAltitude < 0)
throw new MapGenerationException("MaximumAltitude must be >= 0");
if (MinimumTerrainContourSpacing < 0)
throw new MapGenerationException("MinimumTerrainContourSpacing must be >= 0");
if (MinimumCliffLength < 1)
throw new MapGenerationException("MinimumCliffLength must be >= 1");
if (RoadSpacing < 0)
throw new MapGenerationException("RoadSpacing must be >= 0");
if (RoadShrink < 0)
throw new MapGenerationException("RoadShrink must be >= 0");
if (Players < 0)
throw new MapGenerationException("Players must be >= 0");
if (CentralSpawnReservationFraction < 0)
throw new MapGenerationException("CentralSpawnReservationFraction must be >= 0");
if (AreaEntityBonus < 0)
throw new MapGenerationException("PlayableAreaDensityBonus must be >= 0");
if (PlayerCountEntityBonus < 0)
throw new MapGenerationException("PlayerCountDensityBonus must be >= 0");
if (SpawnRegionSize < 1)
throw new MapGenerationException("SpawnRegionSize must be >= 1");
if (SpawnReservation < 1)
throw new MapGenerationException("SpawnReservation must be >= 1");
if (SpawnBuildSize < 1)
throw new MapGenerationException("SpawnBuildSize must be >= 1");
if (MinimumSpawnRadius < 1)
throw new MapGenerationException("MinimumSpawnRadius must be >= 1");
if (SpawnResourceSpawns < 0)
throw new MapGenerationException("SpawnResourceSpawns must be >= 0");
if (ResourceSpawnReservation < 1)
throw new MapGenerationException("ResourceSpawnReservation must be >= 1");
if (MaximumExpansionResourceSpawns < 0)
throw new MapGenerationException("MaximumExpansionResourceSpawns must be >= 0");
if (MinimumExpansionSize < 1)
throw new MapGenerationException("MinimumExpansionSize must be >= 1");
if (MaximumExpansionSize < 1)
throw new MapGenerationException("MaximumExpansionSize must be >= 1");
if (MinimumExpansionSize > MaximumExpansionSize)
throw new MapGenerationException("MinimumExpansionSize must be <= maximumExpansionSize");
if (ExpansionBorder < 1)
throw new MapGenerationException("ExpansionBorder must be >= 1");
if (ExpansionInner < 1)
throw new MapGenerationException("ExpansionInner must be >= 1");
if (MaximumResourceSpawnsPerExpansion < 1)
throw new MapGenerationException("MaximumResourceSpawnsPerExpansion must be >= 1");
if (MinimumBuildings < 0)
throw new MapGenerationException("MinimumBuildings must be >= 0");
if (MaximumBuildings < 0)
throw new MapGenerationException("MaximumBuildings must be >= 0");
if (MinimumBuildings > MaximumBuildings)
throw new MapGenerationException("MinimumBuildings must be <= maximumBuildings");
if (CivilianBuildings < 0 || CivilianBuildings > FractionMax)
throw new MapGenerationException($"CivilianBuildings must be between 0 and {FractionMax} inclusive");
if (CivilianBuildingDensity < 0 || CivilianBuildingDensity > FractionMax)
throw new MapGenerationException($"CivilianBuildingDensity must be between 0 and {FractionMax} inclusive");
if (MinimumCivilianBuildingDensity < 0 || MinimumCivilianBuildingDensity > FractionMax)
throw new MapGenerationException($"MinimumCivilianBuildingDensity must be between 0 and {FractionMax} inclusive");
if (CivilianBuildingDensityRadius < 0)
throw new MapGenerationException("CivilianBuildingDensityRadius must be >= 0");
if (ResourcesPerPlayer < 0)
throw new MapGenerationException("ResourcesPerPlayer must be >= 0");
if (OreUniformity < 0)
throw new MapGenerationException("OreUniformity must be >= 0");
if (OreClumpiness < 0)
throw new MapGenerationException("OreClumpiness must be >= 0");
foreach (var kv in BuildingWeights)
if (kv.Value < 0)
throw new MapGenerationException("BuildingWeights.* must be >= 0");
foreach (var kv in ResourceSpawnWeights)
if (kv.Value < 0)
throw new MapGenerationException("ResourceSpawnWeights.* must be >= 0");
foreach (var kv in ResourceSpawnWeights)
if (!ResourceSpawnSeeds.ContainsKey(kv.Key))
throw new MapGenerationException($"ResourceSpawnSeeds does not contain possible resource spawn `{kv.Key}`");
if (!(terrainInfo.Templates.TryGetValue(LandTile, out var landTemplate) && landTemplate.Contains(0)))
throw new MapGenerationException("LandTile is not valid");
if (!(terrainInfo.Templates.TryGetValue(LandTile, out var waterTemplate) && waterTemplate.Contains(0)))
throw new MapGenerationException("WaterTile is not valid");
if (Players > 32)
throw new MapGenerationException("Total number of players must not exceed 32");
var symmetryCount = Symmetry.RotateAndMirrorProjectionCount(Rotations, Mirror);
if (Players % symmetryCount != 0)
throw new MapGenerationException($"Total number of players must be a multiple of {symmetryCount}");
}
}
public IMapGeneratorSettings GetSettings()
{
return new MapGeneratorSettings(this, Settings);
}
public Map Generate(ModData modData, MapGenerationArgs args)
{
var terrainInfo = modData.DefaultTerrainInfo[args.Tileset];
var size = args.Size;
var map = new Map(modData, terrainInfo, size);
var actorPlans = new List<ActorPlan>();
var param = new Parameters(map, args.Settings);
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");
CellLayer<MultiBrush.Replaceability> PlayableToReplaceable()
{
var playable = terraformer.CheckSpace(param.PlayableTerrain, true);
var basicLand = terraformer.CheckSpace(param.LandTile);
var replace = new CellLayer<MultiBrush.Replaceability>(map);
foreach (var mpos in map.AllCells.MapCoords)
if (playable[mpos])
{
if (basicLand[mpos])
replace[mpos] = MultiBrush.Replaceability.Any;
else
replace[mpos] = MultiBrush.Replaceability.Actor;
}
else
{
replace[mpos] = MultiBrush.Replaceability.None;
}
return replace;
}
// Use `random` to derive separate independent random number generators.
//
// This prevents changes in one part of the algorithm from affecting randomness in
// other parts and provides flexibility for future parallel processing.
//
// In order to maximize stability, additions should be appended only. Disused
// derivatives may be deleted but should be replaced with their unused call to
// random.Next(). All generators should be created unconditionally.
var random = new MersenneTwister(param.Seed);
var elevationRandom = new MersenneTwister(random.Next());
var beachTilingRandom = new MersenneTwister(random.Next());
var cliffTilingRandom = new MersenneTwister(random.Next());
var forestRandom = new MersenneTwister(random.Next());
var forestTilingRandom = new MersenneTwister(random.Next());
var symmetryTilingRandom = new MersenneTwister(random.Next());
var debrisTilingRandom = new MersenneTwister(random.Next());
var resourceRandom = new MersenneTwister(random.Next());
var roadTilingRandom = new MersenneTwister(random.Next());
var playerRandom = new MersenneTwister(random.Next());
var expansionRandom = new MersenneTwister(random.Next());
var buildingRandom = new MersenneTwister(random.Next());
var topologyRandom = new MersenneTwister(random.Next());
var repaintRandom = new MersenneTwister(random.Next());
var decorationRandom = new MersenneTwister(random.Next());
var decorationTilingRandom = new MersenneTwister(random.Next());
var pickAnyRandom = new MersenneTwister(random.Next());
terraformer.InitMap();
foreach (var mpos in map.AllCells.MapCoords)
map.Tiles[mpos] = terraformer.PickTile(pickAnyRandom, param.LandTile);
var elevation = terraformer.ElevationNoiseMatrix(
elevationRandom,
param.TerrainFeatureSize,
param.TerrainSmoothing);
Matrix<bool> mapShape;
if (param.ExternalCircularBias == 0)
mapShape = new Matrix<bool>(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)
{
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);
}
landPlan = MatrixUtils.BooleanBlotch(
landPlan,
param.TerrainSmoothing,
param.SmoothingThreshold, /*smoothingThresholdOutOf=*/FractionMax,
param.MinimumLandSeaThickness,
/*bias=*/param.Water <= FractionMax / 2);
var beaches = CellLayerUtils.FromMatrixPoints(
MatrixUtils.BordersToPoints(landPlan),
map.Tiles);
var beachPaths = beaches
.Select(beach =>
TilingPath.QuickCreate(
map,
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)
{
var roughnessMatrix = MatrixUtils.GridVariance(
elevation,
param.RoughnessRadius);
var cliffMask = MatrixUtils.CalibratedBooleanThreshold(
roughnessMatrix,
param.Roughness, FractionMax);
var cliffPlan = Matrix<bool>.Zip(landPlan, mapShape, (a, b) => a && b);
for (var altitude = 0; altitude < param.MaximumAltitude; altitude++)
{
cliffPlan = terraformer.SliceElevation(
elevation,
cliffPlan,
param.Mountains,
param.MinimumTerrainContourSpacing);
cliffPlan = MatrixUtils.BooleanBlotch(
cliffPlan,
param.TerrainSmoothing,
param.SmoothingThreshold, /*smoothingThresholdOutOf=*/FractionMax,
param.MinimumMountainThickness,
/*bias=*/false);
var unmaskedCliffs = MatrixUtils.BordersToPoints(cliffPlan);
var maskedCliffs = MatrixUtils.MaskPathPoints(unmaskedCliffs, cliffMask);
var cliffs = CellLayerUtils.FromMatrixPoints(maskedCliffs, map.Tiles)
.Where(cliff => cliff.Length >= param.MinimumCliffLength).ToArray();
if (cliffs.Length == 0)
break;
foreach (var cliff in cliffs)
{
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");
terraformer.PaintTiling(pickAnyRandom, brush);
}
}
}
if (param.Forests > 0)
{
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,
param.ForestFeatureSize,
param.Forests,
param.ForestClumpiness);
var replace = PlayableToReplaceable();
foreach (var mpos in map.AllCells.MapCoords)
if (!forestNoise[mpos] || !space[mpos] || passages[mpos])
replace[mpos] = MultiBrush.Replaceability.None;
terraformer.PaintArea(forestTilingRandom, replace, param.ForestObstacles);
}
if (param.EnforceSymmetry != 0)
{
var asymmetries = terraformer.FindAsymmetries(param.DominantTerrain, true, param.EnforceSymmetry == 2);
terraformer.PaintActors(symmetryTilingRandom, asymmetries, param.ForestObstacles);
}
CellLayer<bool> playable;
{
// For circle-in-mountains, the outside is unplayable and should never count as
// the largest/preferred region.
CellLayer<bool> poison = null;
if (param.ExternalCircularBias > 0)
poison = terraformer.CenteredCircle(
false, true, CellLayerUtils.Radius(map.Tiles) - new WDist(1024));
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 (playable.Count(p => p) < minimumPlayableSpace)
throw new MapGenerationException("playable space is too small");
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 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 replace = PlayableToReplaceable();
foreach (var mpos in map.AllCells.MapCoords)
if (playable[mpos] || !map.Contains(mpos))
replace[mpos] = MultiBrush.Replaceability.None;
terraformer.PaintArea(debrisTilingRandom, replace, param.UnplayableObstacles);
}
}
if (param.Roads)
{
// TODO: Move or collapse into configuration
const int RoadMinimumShrinkLength = 12;
const int RoadStraightenShrink = 4;
const int RoadStraightenGrow = 2;
const int RoadInertialRange = 8;
var roadPaths = terraformer.PlanRoads(
terraformer.CheckSpace(param.ClearTerrain, true, false),
param.RoadSpacing,
RoadMinimumShrinkLength + 2 * (RoadStraightenShrink + param.RoadShrink));
foreach (var roadPath in roadPaths)
{
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 = tilingPath.Tile(roadTilingRandom)
?? throw new MapGenerationException("Could not fit tiles for roads");
terraformer.PaintTiling(pickAnyRandom, brush);
}
}
if (param.CreateEntities)
{
var zoneable = terraformer.GetZoneable(param.ZoneableTerrain, playable);
var zoneableArea = zoneable.Count(v => v);
var symmetryCount = Symmetry.RotateAndMirrorProjectionCount(param.Rotations, param.Mirror);
var entityMultiplier =
(long)zoneableArea * param.AreaEntityBonus +
(long)param.Players * param.PlayerCountEntityBonus;
var perSymmetryEntityMultiplier = entityMultiplier / symmetryCount;
// Spawn generation
var symmetryPlayers = param.Players / symmetryCount;
for (var iteration = 0; iteration < symmetryPlayers; iteration++)
{
var chosenCPos = terraformer.ChooseSpawnInZoneable(
playerRandom,
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 = chosenCPos,
};
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,
resourceSpawnPreferences,
param.ResourceSpawnWeights,
param.SpawnResourceSpawns,
false,
new WDist(param.ResourceSpawnReservation * 1024));
terraformer.ProjectPlaceDezoneActor(spawn, zoneable, new WDist(param.SpawnReservation * 1024));
}
// Expansions
{
var resourceSpawnsRemaining = (int)(param.MaximumExpansionResourceSpawns * perSymmetryEntityMultiplier / EntityBonusMax);
while (resourceSpawnsRemaining > 0)
{
var added = terraformer.AddActorCluster(
expansionRandom,
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;
}
}
// Neutral buildings
{
var (buildingTypes, buildingWeights) = Terraformer.SplitDictionary(param.BuildingWeights);
var targetBuildingCount =
(param.MaximumBuildings != 0)
? buildingRandom.Next(
(int)(param.MinimumBuildings * perSymmetryEntityMultiplier / EntityBonusMax),
(int)(param.MaximumBuildings * perSymmetryEntityMultiplier / EntityBonusMax) + 1)
: 0;
for (var i = 0; i < targetBuildingCount; i++)
terraformer.AddActor(
buildingRandom,
zoneable,
buildingTypes[buildingRandom.PickWeighted(buildingWeights)]);
}
// Grow resources
var targetResourceValue = param.ResourcesPerPlayer * entityMultiplier / EntityBonusMax;
if (targetResourceValue > 0)
{
var resourcePattern = terraformer.ResourceNoise(
resourceRandom,
param.ResourceFeatureSize,
param.OreClumpiness,
param.OreUniformity * 1024 / FractionMax);
var resourceBiases = new List<Terraformer.ResourceBias>();
var wSpawnBuildSizeSq = (long)param.SpawnBuildSize * param.SpawnBuildSize * 1024 * 1024;
// Bias towards resource spawns
foreach (var (actorType, resourceType) in param.ResourceSpawnSeeds.OrderBy(kv => kv.Key))
{
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,
}));
}
// 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),
}));
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 decorationNoise = terraformer.DecorationPattern(
decorationRandom,
terraformer.CheckSpace(param.PlayableTerrain, true),
CellLayerUtils.Intersect([zoneable, terraformer.CheckSpace(param.LandTile)]),
param.CivilianBuildings,
param.CivilianBuildingsFeatureSize,
param.CivilianBuildingDensity,
param.MinimumCivilianBuildingDensity,
param.CivilianBuildingDensityRadius);
terraformer.PaintActors(
decorationTilingRandom,
decorationNoise,
param.CivilianBuildingsObstacles,
alwaysPreferLargerBrushes: true);
}
}
// Cosmetically repaint tiles
terraformer.RepaintTiles(repaintRandom, param.RepaintTiles);
terraformer.BakeMap();
return map;
}
string IEditorToolInfo.Label => Name;
string IEditorToolInfo.PanelWidget => PanelWidget;
}
public class ExperimentalMapGenerator { /* we're only interested in the Info */ }
}