Files
OpenRA/OpenRA.Mods.Common/Traits/World/RaMapGenerator.cs
Ashley Newson 9d77ca2bf8 Generate more elaborate roads
Updates the random map generator to produce more elaborate roads:

- Roads extend beyond map bounds.
- Shorter, unviable roads are pruned and merged to create longer roads.
- Roads can loop.
2025-01-18 13:32:56 +02:00

1636 lines
58 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.Primitives;
using OpenRA.Support;
using OpenRA.Traits;
using static OpenRA.Mods.Common.Traits.ResourceLayerInfo;
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.World | SystemActors.EditorWorld)]
public sealed class RaMapGeneratorInfo : TraitInfo, IMapGeneratorInfo
{
[FieldLoader.Require]
public readonly string Type = null;
[FieldLoader.Require]
[FluentReference]
public readonly string Name = null;
// 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;
public override object Create(ActorInitializer init) { return new RaMapGenerator(this); }
static MiniYaml SettingsLoader(MiniYaml my)
{
return my.NodeWithKey("Settings").Value;
}
static List<string> FluentReferencesLoader(MiniYaml my)
{
return MapGeneratorSettings.DumpFluent(my.NodeWithKey("Settings").Value);
}
}
public sealed class RaMapGenerator : IMapGenerator
{
sealed class Parameters
{
[FieldLoader.Ignore]
public readonly Map Map;
[FieldLoader.Ignore]
public readonly ITemplatedTerrainInfo TemplatedTerrainInfo;
[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 float TerrainFeatureSize = default;
[FieldLoader.Require]
public readonly float ForestFeatureSize = default;
[FieldLoader.Require]
public readonly float ResourceFeatureSize = default;
[FieldLoader.Require]
public readonly float Water = default;
[FieldLoader.Require]
public readonly float Mountains = default;
[FieldLoader.Require]
public readonly float 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 float 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 float Roughness = default;
[FieldLoader.Require]
public readonly int MinimumTerrainContourSpacing = default;
[FieldLoader.Require]
public readonly int MinimumCliffLength = default;
[FieldLoader.Require]
public readonly float 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 float AreaEntityBonus = default;
[FieldLoader.Require]
public readonly float PlayerCountEntityBonus = default;
[FieldLoader.Require]
public readonly float 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 SpawnResourceSpawns = default;
[FieldLoader.Require]
public readonly int SpawnReservation = default;
[FieldLoader.Require]
public readonly float SpawnResourceBias = default;
[FieldLoader.Require]
public readonly int ResourcesPerPlayer = default;
[FieldLoader.Require]
public readonly float OreUniformity = default;
[FieldLoader.Require]
public readonly float 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, float> BuildingWeights = default;
[FieldLoader.Require]
public readonly ushort LandTile = default;
[FieldLoader.Require]
public readonly ushort WaterTile = default;
[FieldLoader.Ignore]
public readonly IReadOnlyList<MultiBrush> ForestObstacles;
[FieldLoader.Ignore]
public readonly IReadOnlyList<MultiBrush> UnplayableObstacles;
[FieldLoader.Ignore]
public readonly IReadOnlyDictionary<string, ResourceTypeInfo> ResourceTypes;
[FieldLoader.Ignore]
public readonly ResourceTypeInfo DefaultResource;
[FieldLoader.Ignore]
public readonly IReadOnlyDictionary<ResourceTypeInfo, int> ResourceValues;
[FieldLoader.Ignore]
public readonly IReadOnlySet<(ResourceTypeInfo, byte)> AllowedTerrainResourceCombos;
[FieldLoader.Ignore]
public readonly IReadOnlyDictionary<string, ResourceTypeInfo> ResourceSpawnSeeds;
[FieldLoader.LoadUsing(nameof(ResourceSpawnWeightsLoader))]
public readonly IReadOnlyDictionary<string, float> ResourceSpawnWeights = default;
[FieldLoader.Ignore]
public readonly IReadOnlySet<byte> ClearTerrain;
[FieldLoader.Ignore]
public readonly IReadOnlySet<byte> PlayableTerrain;
[FieldLoader.Ignore]
public readonly IReadOnlySet<byte> PartiallyPlayableTerrain;
[FieldLoader.Ignore]
public readonly IReadOnlySet<byte> UnplayableTerrain;
[FieldLoader.Ignore]
public readonly IReadOnlySet<byte> DominantTerrain;
[FieldLoader.Ignore]
public readonly IReadOnlySet<string> PartiallyPlayableCategories;
[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;
[FieldLoader.Ignore]
public readonly int SymmetryCount;
[FieldLoader.Ignore]
public readonly int TotalPlayers;
public Parameters(Map map, MiniYaml my)
{
FieldLoader.Load(this, my);
Map = map;
TemplatedTerrainInfo = Map.Rules.TerrainInfo as ITemplatedTerrainInfo;
ForestObstacles = MultiBrush.LoadCollection(map, my.NodeWithKey("ForestObstacles").Value.Value);
UnplayableObstacles = MultiBrush.LoadCollection(map, my.NodeWithKey("UnplayableObstacles").Value.Value);
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>();
ResourceValues = playerResourcesInfo.ResourceValues
.ToDictionary(kv => ResourceTypes[kv.Key], kv => kv.Value);
AllowedTerrainResourceCombos = ResourceTypes
.Values
.SelectMany(resourceTypeInfo => resourceTypeInfo.AllowedTerrainTypes
.Select(terrainName => (resourceTypeInfo, TemplatedTerrainInfo.GetTerrainIndex(terrainName))))
.ToImmutableHashSet();
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(TemplatedTerrainInfo.GetTerrainIndex)
.ToImmutableHashSet();
}
IReadOnlyList<string> ParseSegmentTypes(string key)
{
return my.NodeWithKey(key).Value.Value
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.ToImmutableArray();
}
ClearTerrain = ParseTerrainIndexes("ClearTerrain");
PlayableTerrain = ParseTerrainIndexes("PlayableTerrain");
PartiallyPlayableTerrain = ParseTerrainIndexes("PartiallyPlayableTerrain");
UnplayableTerrain = ParseTerrainIndexes("UnplayableTerrain");
DominantTerrain = ParseTerrainIndexes("DominantTerrain");
PartiallyPlayableCategories = my.NodeWithKey("PartiallyPlayableCategories").Value.Value
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.ToImmutableHashSet();
ClearSegmentTypes = ParseSegmentTypes("ClearSegmentTypes");
BeachSegmentTypes = ParseSegmentTypes("BeachSegmentTypes");
CliffSegmentTypes = ParseSegmentTypes("CliffSegmentTypes");
RoadSegmentTypes = ParseSegmentTypes("RoadSegmentTypes");
SymmetryCount = Symmetry.RotateAndMirrorProjectionCount(Rotations, Mirror);
TotalPlayers = Players * SymmetryCount;
Validate();
}
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, float> BuildingWeightsLoader(MiniYaml my)
{
return my.NodeWithKey("BuildingWeights").Value.ToDictionary(subMy =>
{
if (Exts.TryParseFloatOrPercentInvariant(subMy.Value, out var f))
return f;
else
throw new YamlException($"Invalid building weight `{subMy.Value}`");
});
}
static IReadOnlyDictionary<string, float> ResourceSpawnWeightsLoader(MiniYaml my)
{
return my.NodeWithKey("ResourceSpawnWeights").Value.ToDictionary(subMy =>
{
if (Exts.TryParseFloatOrPercentInvariant(subMy.Value, out var f))
return f;
else
throw new YamlException($"Invalid resource spawn weight `{subMy.Value}`");
});
}
public void Validate()
{
if (Rotations < 1)
throw new MapGenerationException("Rotations must be >= 1");
if (TerrainFeatureSize < 1.0f)
throw new MapGenerationException("TerrainFeatureSize must be >= 1.0");
if (ForestFeatureSize < 1.0f)
throw new MapGenerationException("ForestFeatureSize must be >= 1.0");
if (ResourceFeatureSize < 1.0f)
throw new MapGenerationException("ResourceFeatureSize must be >= 1.0");
if (TerrainSmoothing < 1)
throw new MapGenerationException("TerrainSmoothing must be < 1");
if (SmoothingThreshold < 0.5f || SmoothingThreshold > 1.0f)
throw new MapGenerationException("SmoothingThreshold must be between 0.5 and 1.0 inclusive");
if (MinimumLandSeaThickness < 1)
throw new MapGenerationException("MinimumLandSeaThickness must be >= 1");
if (MinimumMountainThickness < 1)
throw new MapGenerationException("MinimumMountainThickness must be >= 1");
if (Water < 0.0f || Water > 1.0f)
throw new MapGenerationException("Water must be between 0.0 and 1.0 inclusive");
if (Forests < 0.0f || Forests > 1.0f)
throw new MapGenerationException("Forest must be between 0.0 and 1.0 inclusive");
if (ForestCutout < 0)
throw new MapGenerationException("ForestCutout must be >= 0");
if (MaximumCutoutSpacing < 0)
throw new MapGenerationException("TopologyAugmentationThreshold must be >= 0");
if (ForestClumpiness < 0.0f)
throw new MapGenerationException("ForestClumpiness must be >= 0.0");
if (Mountains < 0.0f || Mountains > 1.0f)
throw new MapGenerationException("Mountains must be between 0.0 and 1.0 inclusive");
if (Roughness < 0.0f || Roughness > 1.0f)
throw new MapGenerationException("Roughness must be between 0.0 and 1.0");
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.0f)
throw new MapGenerationException("CentralSpawnReservationFraction must be >= 0.0");
if (AreaEntityBonus < 0.0f)
throw new MapGenerationException("PlayableAreaDensityBonus must be >= 0.0");
if (PlayerCountEntityBonus < 0.0f)
throw new MapGenerationException("PlayerCountDensityBonus must be >= 0.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 (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 (ResourcesPerPlayer < 0)
throw new MapGenerationException("ResourcesPerPlayer must be >= 0");
if (OreUniformity < 0.0f)
throw new MapGenerationException("OreUniformity must be >= 0.0");
if (OreClumpiness < 0.0f)
throw new MapGenerationException("OreClumpiness must be >= 0.0");
foreach (var kv in BuildingWeights)
if (kv.Value < 0.0f)
throw new MapGenerationException("BuildingWeights.* must be >= 0.0");
foreach (var kv in ResourceSpawnWeights)
if (kv.Value < 0.0f)
throw new MapGenerationException("ResourceSpawnWeights.* must be >= 0.0");
foreach (var kv in ResourceSpawnWeights)
if (!ResourceSpawnSeeds.ContainsKey(kv.Key))
throw new MapGenerationException($"ResourceSpawnSeeds does not contain possible resource spawn `{kv.Key}`");
if (!(TemplatedTerrainInfo.Templates.TryGetValue(LandTile, out var landTemplate) && landTemplate.Contains(0)))
throw new MapGenerationException("LandTile is not valid");
if (!(TemplatedTerrainInfo.Templates.TryGetValue(LandTile, out var waterTemplate) && waterTemplate.Contains(0)))
throw new MapGenerationException("WaterTile is not valid");
if (TotalPlayers > 32)
throw new MapGenerationException("Total number of players must not exceed 32");
}
public static (T[] Types, float[] Weights) SplitWeights<T>(IReadOnlyDictionary<T, float> typeWeights)
{
var types = typeWeights
.Select(kv => kv.Key)
.OrderBy(k => k)
.ToArray();
var weights = types
.Select(type => typeWeights[type])
.ToArray();
return (types, weights);
}
}
readonly RaMapGeneratorInfo info;
IMapGeneratorInfo IMapGenerator.Info => info;
public RaMapGenerator(RaMapGeneratorInfo info)
{
this.info = info;
}
public MapGeneratorSettings GetSettings(Map map)
{
return MapGeneratorSettings.LoadSettings(info.Settings, map);
}
public void Generate(Map map, MiniYaml settings)
{
const float ExternalBias = 1000000.0f;
var size = map.MapSize;
var minSpan = Math.Min(size.X, size.Y);
var maxSpan = Math.Max(size.X, size.Y);
var mapCenter = (size.ToFloat2() - new float2(1.0f, 1.0f)) / 2.0f;
var wMapCenter = CellLayerUtils.Center(map.Tiles);
var matrixMapCenter = (CellLayerUtils.CellBounds(map).Size.ToInt2().ToFloat2() - new float2(1.0f, 1.0f)) / 2.0f;
var cellBounds = CellLayerUtils.CellBounds(map);
var minCSpan = Math.Min(cellBounds.Size.Width, cellBounds.Size.Height);
var gridType = map.Grid.Type;
var actorPlans = new List<ActorPlan>();
var param = new Parameters(map, settings);
var externalCircleRadius = minCSpan / 2 - (param.MinimumLandSeaThickness + param.MinimumMountainThickness);
if (externalCircleRadius <= 0)
throw new MapGenerationException("map is too small for circular shaping");
var trivialMirror =
size.X == size.Y ||
param.Mirror == Symmetry.Mirror.None ||
param.Mirror == Symmetry.Mirror.LeftMatchesRight ||
param.Mirror == Symmetry.Mirror.TopMatchesBottom;
var tileset = param.TemplatedTerrainInfo;
var beachPermittedTemplates =
TilingPath.PermittedSegments.FromType(tileset, param.BeachSegmentTypes);
var beachTiles = beachPermittedTemplates.PossibleTiles().ToImmutableHashSet();
var replaceabilityMap = new Dictionary<TerrainTile, MultiBrush.Replaceability>();
var playabilityMap = new Dictionary<TerrainTile, PlayableSpace.Playability>();
foreach (var kv in tileset.Templates)
{
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 = tileset.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)
{
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;
else
replaceabilityMap[tile] = MultiBrush.Replaceability.Actor;
if (param.PartiallyPlayableCategories.Overlaps(template.Categories)
&& playabilityMap[tile] == PlayableSpace.Playability.Unplayable)
{
playabilityMap[tile] = PlayableSpace.Playability.Partial;
}
}
}
}
// 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 pickAnyRandom = new MersenneTwister(random.Next());
var waterRandom = 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());
TerrainTile PickTile(ushort tileType)
{
if (tileset.Templates.TryGetValue(tileType, out var template) && template.PickAny)
return new TerrainTile(tileType, (byte)random.Next(0, template.TilesCount));
else
return new TerrainTile(tileType, 0);
}
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;
}
var elevation = NoiseUtils.SymmetricFractalNoise(
waterRandom,
cellBounds.Size.ToInt2(),
param.Rotations,
param.Mirror,
param.TerrainFeatureSize,
NoiseUtils.PinkAmplitude);
if (param.TerrainSmoothing > 0)
{
var radius = param.TerrainSmoothing;
elevation = MatrixUtils.GaussianBlur(elevation, radius, radius);
}
MatrixUtils.CalibrateQuantileInPlace(
elevation,
0.0f,
param.Water);
if (param.ExternalCircularBias != 0)
MatrixUtils.OverCircle(
matrix: elevation,
center: mapCenter,
radius: externalCircleRadius,
outside: true,
action: (xy, _) => elevation[xy] = param.ExternalCircularBias * ExternalBias);
var landPlan = MatrixUtils.BooleanBlotch(
elevation.Map(v => v >= 0),
param.TerrainSmoothing,
param.SmoothingThreshold,
param.MinimumLandSeaThickness,
/*bias=*/param.Water < 0.5);
var beaches = CellLayerUtils.FromMatrixPoints(
MatrixUtils.BordersToPoints(landPlan),
map.Tiles);
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)
.OptimizeLoop();
tiledBeaches[i] =
beachPath.Tile(beachTilingRandom)
?? throw new MapGenerationException("Could not fit tiles for beach");
}
var beachChiralityMatrix = MatrixUtils.PointsChirality(
landPlan.Size,
CellLayerUtils.ToMatrixPoints(tiledBeaches, map.Tiles));
var beachChirality = new CellLayer<int>(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 && map.Tiles[mpos].Type == param.LandTile)
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(
tileset, param.CliffSegmentTypes, param.ClearSegmentTypes);
var loopedCliffPermittedTemplates =
TilingPath.PermittedSegments.FromType(
tileset, param.CliffSegmentTypes);
if (param.ExternalCircularBias > 0)
{
var cliffRing = new CellLayer<bool>(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(
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)
.OptimizeLoop();
if (cliffPath.Tile(cliffTilingRandom) == null)
throw new MapGenerationException("Could not fit tiles for exterior circle cliffs");
}
}
if (param.Mountains > 0.0f || param.ExternalCircularBias == 1)
{
var roughnessMatrix = MatrixUtils.GridVariance(
elevation,
param.RoughnessRadius)
.Map(v => MathF.Sqrt(v));
MatrixUtils.CalibrateQuantileInPlace(
roughnessMatrix,
0.0f,
1.0f - param.Roughness);
var cliffMask = roughnessMatrix.Map(v => v >= 0.0f);
var mountainElevation = elevation.Clone();
var cliffPlan = landPlan;
if (param.ExternalCircularBias > 0)
MatrixUtils.OverCircle(
matrix: cliffPlan,
center: matrixMapCenter,
radius: externalCircleRadius,
outside: true,
action: (xy, _) => cliffPlan[xy] = false);
for (var altitude = 1; 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.X * size.Y;
for (var n = 0; n < mountainElevation.Data.Length; n++)
{
if (roominess.Data[n] < param.MinimumTerrainContourSpacing)
mountainElevation.Data[n] = -1.0f;
else
available++;
total++;
}
var availableFraction = (float)available / total;
MatrixUtils.CalibrateQuantileInPlace(
mountainElevation,
0.0f,
1.0f - availableFraction * param.Mountains);
cliffPlan = MatrixUtils.BooleanBlotch(
mountainElevation.Map(v => v >= 0),
param.TerrainSmoothing,
param.SmoothingThreshold,
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 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)
.OptimizeLoop();
if (cliffPath.Tile(cliffTilingRandom) == null)
throw new MapGenerationException("Could not fit tiles for cliffs");
}
}
}
if (param.Forests > 0.0f)
{
var forestNoise = new CellLayer<float>(map);
NoiseUtils.SymmetricFractalNoiseIntoCellLayer(
forestRandom,
forestNoise,
param.Rotations,
param.Mirror,
param.ForestFeatureSize,
wavelength => MathF.Pow(wavelength, param.ForestClumpiness));
CellLayerUtils.CalibrateQuantileInPlace(
forestNoise,
0.0f,
1.0f - param.Forests);
var forestPlan = new CellLayer<bool>(map);
foreach (var mpos in map.AllCells.MapCoords)
if (param.ClearTerrain.Contains(map.GetTerrainIndex(mpos)) && forestNoise[mpos] >= 0.0f)
forestPlan[mpos] = true;
if (param.ForestCutout > 0)
{
var space = new CellLayer<bool>(map);
foreach (var mpos in map.AllCells.MapCoords)
space[mpos] = param.ClearTerrain.Contains(map.GetTerrainIndex(mpos));
// Improve symmetry.
{
var newSpace = new CellLayer<bool>(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<int>(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<bool>(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<bool>(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<MultiBrush.Replaceability>(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 (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<MultiBrush.Replaceability>(map);
Symmetry.RotateAndMirrorOverCPos(
replace,
param.Rotations,
param.Mirror,
(CPos[] sources, CPos destination) =>
{
var main = tileset.GetTerrainIndex(map.Tiles[destination]);
var compatible = sources
.Where(replace.Contains)
.Select(source => tileset.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 playableArea = new CellLayer<bool>(map);
{
var (regions, regionMask, playability) = PlayableSpace.FindPlayableRegions(map, actorPlans, playabilityMap);
PlayableSpace.Region largest = null;
var disqualifications = new HashSet<int>();
// For circle-in-mountains, the outside is unplayable and should never count as
// the largest/preferred region.
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]);
});
}
// 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");
var minimumPlayableSpace = (int)(param.TotalPlayers * Math.PI * param.SpawnBuildSize * param.SpawnBuildSize);
if (largest.PlayableArea < 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.
{
var unplayableWater = new HashSet<CPos>();
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 =
map.Tiles[mpos].Type == param.WaterTile ||
beachTiles.Contains(map.Tiles[mpos]);
map.Tiles[mpos] = PickTile(param.LandTile);
regionMask[mpos] = PlayableSpace.NullRegion;
return propagate ? false : null;
}
CellLayerUtils.FloodFill(
map.Tiles,
unplayableWater.Select(cpos => (cpos, false)),
ClearWaterBody,
Direction.Spread4CVec);
}
var replaceable = IdentifyReplaceableTiles(map, replaceabilityMap, actorPlans);
var replace = new CellLayer<MultiBrush.Replaceability>(map);
foreach (var mpos in map.AllCells.MapCoords)
if (regionMask[mpos] == largest.Id || !map.Contains(mpos))
replace[mpos] = MultiBrush.Replaceability.None;
else
replace[mpos] = replaceable[mpos];
MultiBrush.PaintArea(map, actorPlans, replace, param.UnplayableObstacles, debrisTilingRandom);
}
foreach (var mpos in map.AllCells.MapCoords)
playableArea[mpos] = playability[mpos] == PlayableSpace.Playability.Playable && regionMask[mpos] == largest.Id;
}
if (param.Roads)
{
var space = new CellLayer<bool>(map);
foreach (var mpos in map.AllCells.MapCoords)
space[mpos] = param.ClearTerrain.Contains(tileset.GetTerrainIndex(map.Tiles[mpos]));
foreach (var actorPlan in actorPlans)
foreach (var (cpos, _) in actorPlan.Footprint())
if (space.Contains(cpos))
space[cpos] = false;
// Improve symmetry.
{
var newSpace = new CellLayer<bool>(map);
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
const int RoadStraightenShrink = 4;
const int RoadStraightenGrow = 2;
const int RoadMinimumShrinkLength = 12;
const int RoadInertialRange = 8;
var roadTotalShrink = RoadStraightenShrink + param.RoadShrink;
var matrixSpace = CellLayerUtils.ToMatrix(space, true);
var kernel = new Matrix<bool>(param.RoadSpacing * 2 + 1, param.RoadSpacing * 2 + 1);
MatrixUtils.OverCircle(
matrix: kernel,
center: new float2(param.RoadSpacing, param.RoadSpacing),
radius: param.RoadSpacing,
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);
var matrixPointArrays = MatrixUtils.DirectionMapToPathsWithPruning(
input: deflated,
minimumLength: 20 + 2 * param.RoadShrink,
minimumJunctionSeparation: 6,
preserveEdgePaths: true);
var pointArrays = CellLayerUtils.FromMatrixPoints(matrixPointArrays, space);
pointArrays = TilingPath.RetainDisjointPaths(pointArrays);
var nonLoopedRoadPermittedTemplates =
TilingPath.PermittedSegments.FromInnerAndTerminalTypes(
tileset, param.RoadSegmentTypes, param.ClearSegmentTypes);
var loopedRoadPermittedTemplates =
TilingPath.PermittedSegments.FromType(
tileset, 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)
.OptimizeLoop()
.RetainIfValid();
// Shrinking may have deleted the path.
if (path.Points == null)
continue;
// Roads that are _almost_ vertical or horizontal tile badly. Filter them out.
var minX = path.Points.Min((p) => p.X);
var minY = path.Points.Min((p) => p.Y);
var maxX = path.Points.Max((p) => p.X);
var maxY = path.Points.Max((p) => p.Y);
if (maxX - minX < 6 || maxY - minY < 6)
continue;
if (path.Tile(roadTilingRandom) == null)
throw new MapGenerationException("Could not fit tiles for roads");
}
}
if (param.CreateEntities)
{
var (buildingTypes, buildingWeights) = Parameters.SplitWeights(param.BuildingWeights);
var (resourceSpawnTypes, resourceSpawnWeights) = Parameters.SplitWeights(param.ResourceSpawnWeights);
var projectionSpacing = new CellLayer<int>(map);
Symmetry.RotateAndMirrorOverCPos(
projectionSpacing,
param.Rotations,
param.Mirror,
(projections, cpos) =>
projectionSpacing[cpos] = Symmetry.ProjectionProximity(projections) / 2);
var spawnReservationRadius = minSpan * param.CentralSpawnReservationFraction;
var spawnReservation = new CellLayer<bool>(map);
foreach (var mpos in map.AllCells.MapCoords)
spawnReservation[mpos] = Symmetry.IsCPosNearCenter(
mpos.ToCPos(gridType),
spawnReservation,
spawnReservationRadius,
param.Mirror);
var zoneable = new CellLayer<bool>(map);
foreach (var mpos in map.AllCells.MapCoords)
zoneable[mpos] = playableArea[mpos] && param.ClearTerrain.Contains(tileset.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<bool>(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 != 0)
{
// 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 zoneableArea = zoneable.Count(v => v);
var entityMultiplier =
zoneableArea * param.AreaEntityBonus +
param.TotalPlayers * param.PlayerCountEntityBonus;
var perSymmetryEntityMultiplier = entityMultiplier / param.SymmetryCount;
// Spawn generation
for (var iteration = 0; iteration < param.Players; iteration++)
{
var spawnPreference = new CellLayer<int>(map);
CellLayerUtils.ChebyshevRoom(spawnPreference, zoneable, false);
foreach (var mpos in map.AllCells.MapCoords)
{
var preference = Math.Min(spawnPreference[mpos], projectionSpacing[mpos]);
if (spawnReservation[mpos] && preference > 1)
preference = 1;
else if (preference > param.SpawnRegionSize)
preference = param.SpawnRegionSize;
spawnPreference[mpos] = preference;
}
var (chosenMPos, chosenValue) = CellLayerUtils.FindRandomBest(
spawnPreference,
playerRandom,
(a, b) => a.CompareTo(b));
if (chosenValue < 1)
throw new MapGenerationException("Not enough room for player spawns");
var room = chosenValue - 1;
var spawn = new ActorPlan(map, "mpspawn")
{
Location = chosenMPos.ToCPos(gridType),
};
var preferedRange = (param.SpawnBuildSize + param.SpawnRegionSize * 2) / 2;
var resourceSpawnPreferences = new CellLayer<float>(map);
CellLayerUtils.WalkingDistances(
resourceSpawnPreferences,
zoneable,
new[] { chosenMPos.ToCPos(gridType) },
param.SpawnRegionSize);
foreach (var mpos in map.AllCells.MapCoords)
{
var v = resourceSpawnPreferences[mpos];
resourceSpawnPreferences[mpos] = MathF.Ceiling(
v > preferedRange ? 2 * preferedRange - v : v);
}
var resourceSpawns = new List<ActorPlan>();
for (var resourceSpawn = 0; resourceSpawn < param.SpawnResourceSpawns; resourceSpawn++)
{
var (mpos, value) = CellLayerUtils.FindRandomBest(
resourceSpawnPreferences,
playerRandom,
(a, b) => a.CompareTo(b));
if (value <= 1.0f)
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.0f);
}
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.SpawnReservation * 1024,
outside: false,
action: (mpos, _, _, _) => zoneable[mpos] = false);
}
// Expansions
{
var resourceSpawnsRemaining = (int)(param.MaximumExpansionResourceSpawns * perSymmetryEntityMultiplier);
while (resourceSpawnsRemaining > 0)
{
var roominess = new CellLayer<int>(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,
expansionRandom,
(a, b) => a.CompareTo(b));
var room = chosenValue - 1;
var radius2 = room - param.ExpansionBorder;
if (radius2 < param.MinimumExpansionSize)
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.0f)
break;
var resourceSpawns = new List<ActorPlan>();
var resourceSpawnPreferences = new CellLayer<float>(map);
var wRadius1Sq = (long)radius1 * radius1 * 1024 * 1024;
CellLayerUtils.OverCircle(
cellLayer: resourceSpawnPreferences,
wCenter: CellLayerUtils.MPosToWPos(chosenMPos, gridType),
wRadius: radius2 * 1024,
outside: false,
action: (mpos, _, _, rSq) =>
resourceSpawnPreferences[mpos] =
rSq >= wRadius1Sq ? rSq : 0.0f);
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.0f);
}
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 targetBuildingCount =
(param.MaximumBuildings != 0)
? expansionRandom.Next(
(int)(param.MinimumBuildings * perSymmetryEntityMultiplier),
(int)(param.MaximumBuildings * perSymmetryEntityMultiplier) + 1)
: 0;
for (var i = 0; i < targetBuildingCount; i++)
{
var roominess = new CellLayer<int>(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,
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);
}
}
// Grow resources
{
var pattern = new CellLayer<float>(map);
NoiseUtils.SymmetricFractalNoiseIntoCellLayer(
resourceRandom,
pattern,
param.Rotations,
param.Mirror,
param.ResourceFeatureSize,
wavelength => MathF.Pow(wavelength, param.OreClumpiness));
{
CellLayerUtils.CalibrateQuantileInPlace(
pattern,
0.0f,
0.0f);
var max = pattern.Max();
foreach (var mpos in map.AllCells.MapCoords)
{
pattern[mpos] /= max;
pattern[mpos] += param.OreUniformity;
}
}
var strengths = new Dictionary<ResourceTypeInfo, CellLayer<float>>();
foreach (var actorPlan in actorPlans)
{
var type = actorPlan.Reference.Type;
if (param.ResourceSpawnWeights.ContainsKey(type))
{
var resource = param.ResourceSpawnSeeds[type];
if (!strengths.TryGetValue(resource, out var strength))
{
strength = new CellLayer<float>(map);
strengths.Add(resource, strength);
}
CellLayerUtils.OverCircle(
cellLayer: strength,
wCenter: actorPlan.WPosLocation,
wRadius: 16 * 1024,
outside: false,
action: (mpos, _, _, rSq) =>
strength[mpos] +=
1024.0f / (1024.0f + MathF.Sqrt(rSq)));
}
}
var maxStrength = new CellLayer<float>(map);
var bestResource = new CellLayer<ResourceTypeInfo>(map);
bestResource.Clear(param.DefaultResource);
foreach (var resourceStrength in strengths)
{
var resource = resourceStrength.Key;
var strength = resourceStrength.Value;
foreach (var mpos in map.AllCells.MapCoords)
if (strength[mpos] > maxStrength[mpos])
{
maxStrength[mpos] = strength[mpos];
bestResource[mpos] = resource;
}
}
// Closer to +inf means "more preferable" for plan.
var plan = new CellLayer<float>(map);
foreach (var mpos in map.AllCells.MapCoords)
if (playableArea[mpos] && param.AllowedTerrainResourceCombos.Contains((bestResource[mpos], map.GetTerrainIndex(mpos))))
plan[mpos] = pattern[mpos] * maxStrength[mpos];
else
plan[mpos] = float.NegativeInfinity;
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] *= 1.0f + param.SpawnResourceBias * wSpawnBuildSizeSq / rSq);
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] = float.NegativeInfinity);
foreach (var actorPlan in actorPlans)
foreach (var (cpos, _) in actorPlan.Footprint())
if (plan.Contains(cpos))
plan[cpos] = float.NegativeInfinity;
// Improve symmetry.
{
var newPlan = new CellLayer<float>(map);
Symmetry.RotateAndMirrorOverCPos(
plan,
param.Rotations,
param.Mirror,
(sources, destination)
=> newPlan[destination] =
sources.Min(source => plan.TryGetValue(source, out var value) ? value : float.NegativeInfinity));
plan = newPlan;
}
var remaining = param.ResourcesPerPlayer * entityMultiplier;
// Closer to -inf means "more preferable" for priorities.
var priorities = new PriorityArray<float>(
plan.Size.Width * plan.Size.Height,
float.PositiveInfinity);
{
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.
// See https://github.com/OpenRA/OpenRA/blob/9302bac6199fbc925a85fd7a08fc2ba4b9317d16/OpenRA.Mods.Common/Traits/World/ResourceLayer.cs#L144-L166
// https://github.com/OpenRA/OpenRA/blob/9302bac6199fbc925a85fd7a08fc2ba4b9317d16/OpenRA.Mods.Common/Traits/World/EditorResourceLayer.cs#L175-L183
int CheckValue(CPos cpos)
{
if (!map.Resources.Contains(cpos))
return 0;
var resource = map.Resources[cpos].Type;
if (resource == 0)
return 0;
var adjacent = 0;
for (var y = -1; y <= 1; y++)
for (var x = -1; x <= 1; x++)
{
var offsetCpos = cpos + new CVec(x, y);
if (!map.Resources.Contains(offsetCpos))
continue;
if (map.Resources[offsetCpos].Type == resource)
adjacent++;
}
var resourceType = bestResource[cpos];
var density = Math.Max(resourceType.MaxDensity * adjacent / /*maxAdjacent=*/9, 1);
// density + 1 to mirror a bug that got ossified due to balancing.
return param.ResourceValues[resourceType] * (density + 1);
}
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)] = float.PositiveInfinity;
// 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] == float.PositiveInfinity)
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);
}
}
}
map.PlayerDefinitions = new MapPlayers(map.Rules, 0).ToMiniYaml();
map.ActorDefinitions = actorPlans
.Select((plan, i) => new MiniYamlNode($"Actor{i}", plan.Reference.Save()))
.ToImmutableArray();
}
static CellLayer<MultiBrush.Replaceability> IdentifyReplaceableTiles(
Map map,
Dictionary<TerrainTile, MultiBrush.Replaceability> replaceabilityMap,
IEnumerable<ActorPlan> actorPlans)
{
var output = new CellLayer<MultiBrush.Replaceability>(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;
}
}
}