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;