diff --git a/OpenRA.Mods.Common/Lint/CheckMultiBrushes.cs b/OpenRA.Mods.Common/Lint/CheckMultiBrushes.cs index c4cd8573ab..850fb91873 100644 --- a/OpenRA.Mods.Common/Lint/CheckMultiBrushes.cs +++ b/OpenRA.Mods.Common/Lint/CheckMultiBrushes.cs @@ -34,7 +34,7 @@ namespace OpenRA.Mods.Common.Lint // Includes validation of actor types and template IDs. var multiBrush = new MultiBrush(map, info); - foreach (var (_, tile) in multiBrush.Tiles) + foreach (var tile in multiBrush.PossibleTiles()) if (!templatedTerrainInfo.TryGetTerrainInfo(tile, out var _)) emitError($"Tileset {terrainInfoName} has invalid MultiBrush collection `{collectionName}`: tileset does not have tile {tile.Type},{tile.Index}"); } diff --git a/OpenRA.Mods.Common/MapGenerator/MultiBrush.cs b/OpenRA.Mods.Common/MapGenerator/MultiBrush.cs index a1483f5aef..c618e96bef 100644 --- a/OpenRA.Mods.Common/MapGenerator/MultiBrush.cs +++ b/OpenRA.Mods.Common/MapGenerator/MultiBrush.cs @@ -234,8 +234,42 @@ namespace OpenRA.Mods.Common.MapGenerator Any = 3, } + readonly struct TileRange + { + public readonly ushort Type; + public readonly byte MinIndex; + public readonly byte MaxIndex; + + public TileRange(ushort type, byte minIndex, byte maxIndex) + { + Type = type; + MinIndex = minIndex; + MaxIndex = maxIndex; + } + + public TileRange(ushort type, byte index) + : this(type, index, index) { } + + public TileRange(TerrainTile tile) + : this(tile.Type, tile.Index) { } + + /// Pick a non-randomized tile. + public TerrainTile DefaultTile => new(Type, MinIndex); + + /// + /// Pick a (possibly randomized) tile. random can be null to fall back to DefaultTile. + /// + public TerrainTile Pick(MersenneTwister random) + { + if (random == null) + return DefaultTile; + + return new TerrainTile(Type, (byte)random.Next(MinIndex, MaxIndex + 1)); + } + } + public int Weight; - readonly List<(CVec, TerrainTile)> tiles; + readonly List<(CVec XY, TileRange TileRange)> tiles; readonly List actorPlans; public MultiBrushSegment Segment { get; private set; } @@ -243,8 +277,6 @@ namespace OpenRA.Mods.Common.MapGenerator // Null means the shape is dirty and must be recomputed. CVec[] shape; - public IEnumerable<(CVec XY, TerrainTile Tile)> Tiles => tiles; - public IEnumerable ActorPlans => actorPlans; public bool HasTiles => tiles.Count != 0; public bool HasActors => actorPlans.Count != 0; public IEnumerable Shape => GetShape(); @@ -378,17 +410,19 @@ namespace OpenRA.Mods.Common.MapGenerator public MultiBrush WithTemplate(TerrainTemplateInfo templateInfo, CVec offset) { if (templateInfo.PickAny) - throw new ArgumentException("PickAny not supported - create separate MultiBrushes using WithTile instead."); - for (var y = 0; y < templateInfo.Size.Y; y++) - for (var x = 0; x < templateInfo.Size.X; x++) - { - var i = y * templateInfo.Size.X + x; - if (templateInfo[i] != null) + { + tiles.Add((offset, new(templateInfo.Id, 0, (byte)(templateInfo.TilesCount - 1)))); + } + else + { + for (var y = 0; y < templateInfo.Size.Y; y++) + for (var x = 0; x < templateInfo.Size.X; x++) { - var tile = new TerrainTile(templateInfo.Id, (byte)i); - tiles.Add((new CVec(x, y) + offset, tile)); + var i = y * templateInfo.Size.X + x; + if (templateInfo[i] != null) + tiles.Add((new CVec(x, y) + offset, new(templateInfo.Id, (byte)i))); } - } + } shape = null; return this; @@ -400,7 +434,7 @@ namespace OpenRA.Mods.Common.MapGenerator /// public MultiBrush WithTile(TerrainTile tile, CVec offset) { - tiles.Add((offset, tile)); + tiles.Add((offset, new(tile))); shape = null; return this; } @@ -422,7 +456,7 @@ namespace OpenRA.Mods.Common.MapGenerator if (Area == 0) throw new InvalidOperationException("No area"); foreach (var xy in shape) - tiles.Add((xy, tile)); + tiles.Add((xy, new(tile))); return this; } @@ -467,9 +501,15 @@ namespace OpenRA.Mods.Common.MapGenerator /// /// Paint tiles onto the map and/or add actors to actorPlans at the given location. /// contract specifies whether tiles or actors are allowed to be painted. + /// An optional MersenneTwister can be provided to vary randomizable elements. /// If nothing could be painted, throws ArgumentException. /// - public void Paint(Map map, List actorPlans, CPos paintAt, Replaceability contract) + public void Paint( + Map map, + List actorPlans, + CPos paintAt, + Replaceability contract, + MersenneTwister random) { switch (contract) { @@ -479,14 +519,14 @@ namespace OpenRA.Mods.Common.MapGenerator if (this.actorPlans.Count > 0) PaintActors(map, actorPlans, paintAt); else if (tiles.Count > 0) - PaintTiles(map, paintAt); + PaintTiles(map, paintAt, random); else throw new ArgumentException("Cannot paint: no tiles or actors"); break; case Replaceability.Tile: if (tiles.Count == 0) throw new ArgumentException("Cannot paint: no tiles"); - PaintTiles(map, paintAt); + PaintTiles(map, paintAt, random); PaintActors(map, actorPlans, paintAt); break; case Replaceability.Actor: @@ -497,13 +537,13 @@ namespace OpenRA.Mods.Common.MapGenerator } } - void PaintTiles(Map map, CPos paintAt) + void PaintTiles(Map map, CPos paintAt, MersenneTwister random) { foreach (var (xy, tile) in tiles) { var mpos = (paintAt + xy).ToMPos(map); if (map.Tiles.Contains(mpos)) - map.Tiles[mpos] = tile; + map.Tiles[mpos] = tile.Pick(random); } } @@ -641,7 +681,7 @@ namespace OpenRA.Mods.Common.MapGenerator var paintAt = mpos.ToCPos(map) - brush.FirstCell; var contract = ReserveShape(paintAt, brush.Shape, brush.Contract()); if (contract != Replaceability.None) - brush.Paint(map, actorPlans, paintAt, contract); + brush.Paint(map, actorPlans, paintAt, contract, random); remainingQuota -= brushArea; if (remainingQuota <= 0) @@ -652,11 +692,13 @@ namespace OpenRA.Mods.Common.MapGenerator /// /// Create a sparse EditorBlitSource from this MultiBrush. The EditorBlitSource will have - /// the minimum bounding CellRegion fully containing all content. For actors without a + /// the minimum bounding CellRegion fully containing all content. An optional + /// MersenneTwister can be provided to vary randomizable elements. For actors without a /// preconfigured owner, a default owner can be specified or derived automatically. /// public EditorBlitSource ToEditorBlitSource( WorldRenderer worldRenderer, + MersenneTwister random, PlayerReference defaultActorOwner = null) { var world = worldRenderer.World; @@ -703,9 +745,10 @@ namespace OpenRA.Mods.Common.MapGenerator } var blitTiles = - Tiles + tiles .Where(t => map.Tiles.Contains(CPos.Zero + t.XY)) .DistinctBy(t => t.XY) + .Select(t => (t.XY, Tile: t.TileRange.Pick(random))) .ToDictionary( t => CPos.Zero + t.XY, t => new BlitTile(t.Tile, default, null, map.Height[CPos.Zero + t.XY])); @@ -715,5 +758,15 @@ namespace OpenRA.Mods.Common.MapGenerator actorPreviews, blitTiles); } + + /// All possible tiles that may be painted by this MultiBrush. + public HashSet PossibleTiles() + { + var possible = new HashSet(); + foreach (var (_, tileRange) in tiles) + for (int i = tileRange.MinIndex; i <= tileRange.MaxIndex; i++) + possible.Add(new(tileRange.Type, (byte)i)); + return possible; + } } } diff --git a/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs b/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs index 8ae428622a..7a9d7550f9 100644 --- a/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs +++ b/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs @@ -601,6 +601,7 @@ namespace OpenRA.Mods.Common.Traits var repaintRandom = new MersenneTwister(random.Next()); var decorationRandom = new MersenneTwister(random.Next()); var decorationTilingRandom = new MersenneTwister(random.Next()); + var pickAnyRandom = new MersenneTwister(random.Next()); TerrainTile PickTile(ushort tileType) { @@ -675,7 +676,7 @@ namespace OpenRA.Mods.Common.Traits .OptimizeLoop(); var brush = beachPath.Tile(beachTilingRandom) ?? throw new MapGenerationException("Could not fit tiles for beach"); - brush.Paint(map, actorPlans, CPos.Zero, MultiBrush.Replaceability.Tile); + brush.Paint(map, actorPlans, CPos.Zero, MultiBrush.Replaceability.Tile, pickAnyRandom); tiledBeaches[i] = brush.Segment.Points.Select(vec => CPos.Zero + vec).ToArray(); foreach (var cvec in brush.Shape) beachesShape.Add(CPos.Zero + cvec); @@ -745,7 +746,7 @@ namespace OpenRA.Mods.Common.Traits .OptimizeLoop(); var brush = cliffPath.Tile(cliffTilingRandom) ?? throw new MapGenerationException("Could not fit tiles for exterior circle cliffs"); - brush.Paint(map, actorPlans, CPos.Zero, MultiBrush.Replaceability.Tile); + brush.Paint(map, actorPlans, CPos.Zero, MultiBrush.Replaceability.Tile, pickAnyRandom); } } @@ -827,7 +828,7 @@ namespace OpenRA.Mods.Common.Traits .OptimizeLoop(); var brush = cliffPath.Tile(cliffTilingRandom) ?? throw new MapGenerationException("Could not fit tiles for cliffs"); - brush.Paint(map, actorPlans, CPos.Zero, MultiBrush.Replaceability.Tile); + brush.Paint(map, actorPlans, CPos.Zero, MultiBrush.Replaceability.Tile, pickAnyRandom); } } } @@ -1277,7 +1278,7 @@ namespace OpenRA.Mods.Common.Traits var brush = path.Tile(roadTilingRandom) ?? throw new MapGenerationException("Could not fit tiles for roads"); - brush.Paint(map, actorPlans, CPos.Zero, MultiBrush.Replaceability.Tile); + brush.Paint(map, actorPlans, CPos.Zero, MultiBrush.Replaceability.Tile, pickAnyRandom); } } diff --git a/OpenRA.Mods.Common/Traits/World/TilingPathTool.cs b/OpenRA.Mods.Common/Traits/World/TilingPathTool.cs index d8209a0ba8..2d589e2f36 100644 --- a/OpenRA.Mods.Common/Traits/World/TilingPathTool.cs +++ b/OpenRA.Mods.Common/Traits/World/TilingPathTool.cs @@ -433,6 +433,7 @@ namespace OpenRA.Mods.Common.Traits foreach (var (startType, endType) in terminalTypes) { + var random = new MersenneTwister(RandomSeed); var tilingPath = new TilingPath( map, points, @@ -442,9 +443,9 @@ namespace OpenRA.Mods.Common.Traits permittedTemplates); tilingPath.Start.Direction = plan.AutoStart; tilingPath.End.Direction = plan.AutoEnd; - var result = tilingPath.Tile(new MersenneTwister(RandomSeed)); + var result = tilingPath.Tile(random); if (result != null) - return result.ToEditorBlitSource(WorldRenderer); + return result.ToEditorBlitSource(WorldRenderer, random); } return null;