Files
OpenRA/OpenRA.Mods.Common/Traits/World/RaMapGenerator.cs
Ashley Newson 2126f3c5a2 Add CnC map generator support
Adds map generator support for CnC (all tilesets), largely on par with
RA.

Most tilesets do not have a complete set of beach templates and
therefore do not support terrain settings involving water. DESERT is the
only tileset supporting water. Unlike in RA, water is not playable space
in CnC (no naval units), so only terrain settings with small bodies of
water are available.

Most changes are configuration (tileset and map generator config), with
just a small number of code changes.
2025-01-20 12:55:36 +02:00

1662 lines
59 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<ushort, IReadOnlyList<MultiBrush>> RepaintTiles;
[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);
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;
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());
var repaintRandom = 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);
}
}
}
// Cosmetically repaint tiles
foreach (var (tile, collection) in param.RepaintTiles.OrderBy(kv => kv.Key))
{
var replace = new CellLayer<MultiBrush.Replaceability>(map);
foreach (var mpos in replace.CellRegion.MapCoords)
replace[mpos] =
map.Tiles[mpos].Type == tile
? MultiBrush.Replaceability.Any
: MultiBrush.Replaceability.None;
MultiBrush.PaintArea(map, actorPlans, replace, collection, repaintRandom);
}
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;
}
}
}