From 4c9ae3678bbaabded284dde813212f52b367f1e3 Mon Sep 17 00:00:00 2001 From: Ashley Newson Date: Sun, 17 Aug 2025 19:47:27 +0100 Subject: [PATCH] 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. --- .../MapGenerator/Terraformer.cs | 9 ++ .../Traits/World/ExperimentalMapGenerator.cs | 100 +++++++++++++----- mods/ra/rules/map-generators.yaml | 7 ++ 3 files changed, 92 insertions(+), 24 deletions(-) diff --git a/OpenRA.Mods.Common/MapGenerator/Terraformer.cs b/OpenRA.Mods.Common/MapGenerator/Terraformer.cs index 710d756b51..ee42b97ba5 100644 --- a/OpenRA.Mods.Common/MapGenerator/Terraformer.cs +++ b/OpenRA.Mods.Common/MapGenerator/Terraformer.cs @@ -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(); if (Vote(0, zones.Length, false, majorities) == 0) diff --git a/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs b/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs index de7b3879a9..740766a9e1 100644 --- a/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs +++ b/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs @@ -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 BeachSegmentTypes; [FieldLoader.Ignore] + public readonly IReadOnlyList WaterCliffSegmentTypes; + [FieldLoader.Ignore] public readonly IReadOnlyList CliffSegmentTypes; [FieldLoader.Ignore] public readonly IReadOnlyList 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 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 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)); } diff --git a/mods/ra/rules/map-generators.yaml b/mods/ra/rules/map-generators.yaml index 95a983ebd2..179d0389c6 100644 --- a/mods/ra/rules/map-generators.yaml +++ b/mods/ra/rules/map-generators.yaml @@ -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: