Add watercliff support to ExperimentalMapGenerator

This commit adds watercliffs to generated maps in the RA mod, based on
the path partitioning logic added for the D2K map generator.

Watercliffs are not available in CnC and unofficial mods, so their use
in the ExperimentalMapGenerator configuration is optional.
This commit is contained in:
Ashley Newson
2025-08-17 19:47:27 +01:00
committed by Gustas Kažukauskas
parent 2cf6ac6383
commit 4c9ae3678b
3 changed files with 92 additions and 24 deletions

View File

@@ -77,6 +77,7 @@ namespace OpenRA.Mods.Common.MapGenerator
public sealed class PathPartitionZone
{
public bool ShouldTile = true;
public bool RequiredSomewhere = false;
public string SegmentType = null;
public int MinimumLength = 1;
public int MaximumDeviation = 0;
@@ -1170,6 +1171,9 @@ namespace OpenRA.Mods.Common.MapGenerator
if (allZones.Count == 0)
throw new ArgumentException("no zones provided");
if (allZones.Count(zone => zone.RequiredSomewhere) > 1)
throw new ArgumentException("RequiredSomewhere only supported for at most one zone");
if (path.Length < 2)
throw new ArgumentException("path is too short");
@@ -1303,6 +1307,11 @@ namespace OpenRA.Mods.Common.MapGenerator
return [];
}
if (allZones.Any(zone => zone.RequiredSomewhere))
{
fallbackPath = allZones.First(zone => zone.RequiredSomewhere);
}
else
{
var majorities = new List<PathPartitionZone>();
if (Vote(0, zones.Length, false, majorities) == 0)

View File

@@ -103,6 +103,7 @@ namespace OpenRA.Mods.Common.Traits
public readonly int TerrainSmoothing = default;
[FieldLoader.Require]
public readonly int SmoothingThreshold = default;
public readonly int MinimumCoastStraight = -1;
[FieldLoader.Require]
public readonly int MinimumLandSeaThickness = default;
[FieldLoader.Require]
@@ -113,8 +114,11 @@ namespace OpenRA.Mods.Common.Traits
public readonly int RoughnessRadius = default;
[FieldLoader.Require]
public readonly int Roughness = default;
public readonly int WaterRoughness = 0;
[FieldLoader.Require]
public readonly int MinimumTerrainContourSpacing = default;
public readonly int MinimumBeachLength = 0;
public readonly int MinimumWaterCliffLength = 0;
[FieldLoader.Require]
public readonly int MinimumCliffLength = default;
[FieldLoader.Require]
@@ -219,6 +223,8 @@ namespace OpenRA.Mods.Common.Traits
[FieldLoader.Ignore]
public readonly IReadOnlyList<string> BeachSegmentTypes;
[FieldLoader.Ignore]
public readonly IReadOnlyList<string> WaterCliffSegmentTypes;
[FieldLoader.Ignore]
public readonly IReadOnlyList<string> CliffSegmentTypes;
[FieldLoader.Ignore]
public readonly IReadOnlyList<string> RoadSegmentTypes;
@@ -291,6 +297,9 @@ namespace OpenRA.Mods.Common.Traits
ClearSegmentTypes = ParseSegmentTypes("ClearSegmentTypes");
BeachSegmentTypes = ParseSegmentTypes("BeachSegmentTypes");
if (WaterRoughness > 0)
WaterCliffSegmentTypes = ParseSegmentTypes("WaterCliffSegmentTypes");
CliffSegmentTypes = ParseSegmentTypes("CliffSegmentTypes");
RoadSegmentTypes = ParseSegmentTypes("RoadSegmentTypes");
@@ -341,6 +350,8 @@ namespace OpenRA.Mods.Common.Traits
throw new MapGenerationException("CivilianBuildingsFeatureSize must be >= 1");
if (TerrainSmoothing < 0 || TerrainSmoothing > MatrixUtils.MaxBinomialKernelRadius)
throw new MapGenerationException($"TerrainSmoothing must be between 0 and {MatrixUtils.MaxBinomialKernelRadius} inclusive");
if (WaterRoughness > 0 && MinimumCoastStraight < 0)
throw new MapGenerationException("MinimumCoastStraight must be >= 0");
if (SmoothingThreshold < (FractionMax + 1) / 2 || SmoothingThreshold > FractionMax)
throw new MapGenerationException($"SmoothingThreshold must be between {(FractionMax + 1) / 2} and {FractionMax} inclusive");
if (MinimumLandSeaThickness < 1)
@@ -361,12 +372,18 @@ namespace OpenRA.Mods.Common.Traits
throw new MapGenerationException($"Mountains must be between 0 and {FractionMax} inclusive");
if (Roughness < 0 || Roughness > FractionMax)
throw new MapGenerationException("Roughness must be between 0 and {FractionMax}");
if (WaterRoughness < 0 || WaterRoughness > FractionMax)
throw new MapGenerationException("WaterRoughness must be between 0 and {FractionMax}");
if (RoughnessRadius < 1)
throw new MapGenerationException("RoughnessRadius must be >= 1");
if (MaximumAltitude < 0)
throw new MapGenerationException("MaximumAltitude must be >= 0");
if (MinimumTerrainContourSpacing < 0)
throw new MapGenerationException("MinimumTerrainContourSpacing must be >= 0");
if (WaterRoughness > 0 && MinimumBeachLength < 1)
throw new MapGenerationException("MinimumBeachLength must be >= 1");
if (WaterRoughness > 0 && MinimumCliffLength < 1)
throw new MapGenerationException("MinimumWaterCliffLength must be >= 1");
if (MinimumCliffLength < 1)
throw new MapGenerationException("MinimumCliffLength must be >= 1");
if (RoadSpacing < 0)
@@ -506,7 +523,7 @@ namespace OpenRA.Mods.Common.Traits
var random = new MersenneTwister(param.Seed);
var elevationRandom = new MersenneTwister(random.Next());
var beachTilingRandom = new MersenneTwister(random.Next());
var coastTilingRandom = new MersenneTwister(random.Next());
var cliffTilingRandom = new MersenneTwister(random.Next());
var forestRandom = new MersenneTwister(random.Next());
var forestTilingRandom = new MersenneTwister(random.Next());
@@ -532,6 +549,9 @@ namespace OpenRA.Mods.Common.Traits
elevationRandom,
param.TerrainFeatureSize,
param.TerrainSmoothing);
var roughnessMatrix = MatrixUtils.GridVariance(
elevation,
param.RoughnessRadius);
Matrix<bool> mapShape;
if (param.ExternalCircularBias == 0)
@@ -565,33 +585,65 @@ namespace OpenRA.Mods.Common.Traits
param.MinimumLandSeaThickness,
/*bias=*/param.Water <= FractionMax / 2);
var beaches = CellLayerUtils.FromMatrixPoints(
MatrixUtils.BordersToPoints(landPlan),
map.Tiles);
var beachPaths = beaches
.Select(beach =>
TilingPath.QuickCreate(
map,
param.SegmentedBrushes,
beach,
(param.MinimumLandSeaThickness - 1) / 2,
param.BeachSegmentTypes[0],
param.BeachSegmentTypes[0])
.ExtendEdge(4))
.ToArray();
var landBeachWater = terraformer.PaintLoopsAndFill(
beachTilingRandom,
beachPaths,
var coast = MatrixUtils.BordersToPoints(landPlan);
List<TilingPath> coastPaths;
if (param.WaterRoughness > 0)
{
var beachZone = new Terraformer.PathPartitionZone()
{
RequiredSomewhere = true,
SegmentType = param.BeachSegmentTypes[0],
MinimumLength = param.MinimumBeachLength,
MaximumDeviation = param.MinimumLandSeaThickness - 1,
};
var waterCliffZone = new Terraformer.PathPartitionZone()
{
SegmentType = param.WaterCliffSegmentTypes[0],
MinimumLength = param.MinimumCliffLength,
MaximumDeviation = param.MinimumLandSeaThickness - 1,
};
var waterCliffMask = MatrixUtils.CalibratedBooleanThreshold(
roughnessMatrix,
param.WaterRoughness, FractionMax);
var partitionMask = waterCliffMask.Map(masked => masked ? waterCliffZone : beachZone);
coastPaths = terraformer.PartitionPaths(
coast,
[beachZone, waterCliffZone],
partitionMask,
param.SegmentedBrushes,
param.MinimumCoastStraight);
foreach (var coastPath in coastPaths)
coastPath
.OptimizeLoop()
.ExtendEdge(4);
}
else
{
coastPaths = CellLayerUtils.FromMatrixPoints(coast, map.Tiles)
.Select(beach =>
TilingPath.QuickCreate(
map,
param.SegmentedBrushes,
beach,
param.MinimumLandSeaThickness - 1,
param.BeachSegmentTypes[0],
param.BeachSegmentTypes[0])
.ExtendEdge(4))
.ToList();
}
var landCoastWater = terraformer.PaintLoopsAndFill(
coastTilingRandom,
coastPaths,
landPlan[0] ? Terraformer.Side.In : Terraformer.Side.Out,
[new MultiBrush().WithTemplate(map, param.WaterTile, CVec.Zero)],
null)
?? throw new MapGenerationException("Could not fit tiles for beach");
?? throw new MapGenerationException("Could not fit tiles for coast");
if (param.Mountains > 0)
{
var roughnessMatrix = MatrixUtils.GridVariance(
elevation,
param.RoughnessRadius);
var cliffMask = MatrixUtils.CalibratedBooleanThreshold(
roughnessMatrix,
param.Roughness, FractionMax);
@@ -679,7 +731,7 @@ namespace OpenRA.Mods.Common.Traits
if (param.DenyWalledAreas)
{
// Beach tiles are particularly problematic. If they're for unplayable bodies
// Coast tiles are particularly problematic. If they're for unplayable bodies
// of water, they should be obliterated. If they're just surrounded by rocks,
// trees, etc, they should be filled in with actors.
if (waterIsPlayable)
@@ -688,7 +740,7 @@ namespace OpenRA.Mods.Common.Traits
terraformer.ZoneFromOutOfBounds(mask, true);
terraformer.FillUnmaskedSideAndBorder(
mask,
landBeachWater,
landCoastWater,
Terraformer.Side.Out,
cpos => map.Tiles[cpos] = terraformer.PickTile(pickAnyRandom, param.LandTile));
}

View File

@@ -18,12 +18,16 @@
MaximumCutoutSpacing: 12
TerrainSmoothing: 4
SmoothingThreshold: 833
MinimumCoastStraight: 1
MinimumLandSeaThickness: 5
MinimumMountainThickness: 5
MaximumAltitude: 8
RoughnessRadius: 5
Roughness: 500
WaterRoughness: 200
MinimumTerrainContourSpacing: 5
MinimumBeachLength: 10
MinimumWaterCliffLength: 10
MinimumCliffLength: 10
ForestClumpiness: 1
DenyWalledAreas: True
@@ -64,6 +68,7 @@
ClearSegmentTypes: Clear
BeachSegmentTypes: Beach
CliffSegmentTypes: Cliff
WaterCliffSegmentTypes: WaterCliff
RoadSegmentTypes: Road
ForestObstacles: Trees
UnplayableObstacles: Obstructions
@@ -74,6 +79,8 @@
Choice@desert:
Tileset: DESERT
Settings:
MinimumBeachLength: 15
MinimumCoastStraight: 3
LandTile: 255
WaterTile: 256
Choice@temperat: