Support PickAny templates in MultiBrushes
Allows PickAny templates to be used in MultiBrushes, which can be painted with random selection. This avoids the need to define various separate MultiBrushes for each tile combination. Note that all combinations count as part of a single MultiBrush, which may influence selection weighting. This MultiBrush feature is not presently used in OpenRA's official mods, but is useful for unofficial mods using more general PickAny templates.
This commit is contained in:
committed by
Gustas Kažukauskas
parent
daf36769d6
commit
5c09d5d828
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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) { }
|
||||
|
||||
/// <summary>Pick a non-randomized tile.</summary>
|
||||
public TerrainTile DefaultTile => new(Type, MinIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Pick a (possibly randomized) tile. random can be null to fall back to DefaultTile.
|
||||
/// </summary>
|
||||
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<ActorPlan> 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<ActorPlan> ActorPlans => actorPlans;
|
||||
public bool HasTiles => tiles.Count != 0;
|
||||
public bool HasActors => actorPlans.Count != 0;
|
||||
public IEnumerable<CVec> 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
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// <para>Paint tiles onto the map and/or add actors to actorPlans at the given location.</para>
|
||||
/// <para>contract specifies whether tiles or actors are allowed to be painted.</para>
|
||||
/// <para>An optional MersenneTwister can be provided to vary randomizable elements.</para>
|
||||
/// <para>If nothing could be painted, throws ArgumentException.</para>
|
||||
/// </summary>
|
||||
public void Paint(Map map, List<ActorPlan> actorPlans, CPos paintAt, Replaceability contract)
|
||||
public void Paint(
|
||||
Map map,
|
||||
List<ActorPlan> 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
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>All possible tiles that may be painted by this MultiBrush.</summary>
|
||||
public HashSet<TerrainTile> PossibleTiles()
|
||||
{
|
||||
var possible = new HashSet<TerrainTile>();
|
||||
foreach (var (_, tileRange) in tiles)
|
||||
for (int i = tileRange.MinIndex; i <= tileRange.MaxIndex; i++)
|
||||
possible.Add(new(tileRange.Type, (byte)i));
|
||||
return possible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user