#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Support;
namespace OpenRA.Mods.Common.MapGenerator
{
///
/// MiniYaml-loaded definition of a MultiBrush. Can be loaded into a MultiBrush once a map is
/// available.
///
public sealed class MultiBrushInfo
{
public readonly float Weight;
public readonly ImmutableArray Actors;
public readonly TerrainTile? BackingTile;
public readonly ImmutableArray Templates;
public readonly ImmutableArray Tiles;
// Currently doesn't support specifying offsets. Add this capability if/when needed.
public MultiBrushInfo(MiniYaml my)
{
Weight = 1.0f;
var actors = new List();
var templates = new List();
var tiles = new List();
foreach (var node in my.Nodes)
switch (node.Key.Split('@')[0])
{
case "Weight":
if (!Exts.TryParseFloatOrPercentInvariant(node.Value.Value, out Weight))
throw new YamlException($"Invalid MultiBrush Weight `${node.Value.Value}`");
break;
case "Actor":
actors.Add(node.Value.Value);
break;
case "BackingTile":
if (TerrainTile.TryParse(node.Value.Value, out var backingTile))
BackingTile = backingTile;
else
throw new YamlException($"Invalid MultiBrush BackingTile `${node.Value.Value}`");
break;
case "Template":
if (Exts.TryParseUshortInvariant(node.Value.Value, out var template))
templates.Add(template);
else
throw new YamlException($"Invalid MultiBrush Template `${node.Value.Value}`");
break;
case "Tile":
if (TerrainTile.TryParse(node.Value.Value, out var tile))
Tiles.Add(tile);
else
throw new YamlException($"Invalid MultiBrush Tile `${node.Value.Value}`");
break;
default:
throw new YamlException($"Unrecognized MultiBrush key {node.Key.Split('@')[0]}");
}
Actors = actors.ToImmutableArray();
Templates = templates.ToImmutableArray();
Tiles = tiles.ToImmutableArray();
}
public static ImmutableArray ParseCollection(MiniYaml my)
{
var brushes = new List();
foreach (var node in my.Nodes)
if (node.Key.Split('@')[0] == "MultiBrush")
brushes.Add(new MultiBrushInfo(node.Value));
else
throw new YamlException($"Expected `MultiBrush@*` but got `{node.Key}`");
return brushes.ToImmutableArray();
}
}
/// A super template that can be used to paint both tiles and actors.
sealed class MultiBrush
{
public enum Replaceability
{
/// Area cannot be replaced by a tile or obstructing actor.
None = 0,
/// Area must be replaced by a different tile, and may optionally be given an actor.
Tile = 1,
/// Area must be given an actor, but the underlying tile must not change.
Actor = 2,
/// Area can be replaced by a tile and/or actor.
Any = 3,
}
public float Weight;
readonly List<(CVec, TerrainTile)> tiles;
readonly List actorPlans;
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 => shape;
public int Area => shape.Length;
public Replaceability Contract()
{
var hasTiles = tiles.Count != 0;
var hasActorPlans = actorPlans.Count != 0;
if (hasTiles && hasActorPlans)
return Replaceability.Any;
else if (hasTiles && !hasActorPlans)
return Replaceability.Tile;
else if (!hasTiles && hasActorPlans)
return Replaceability.Actor;
else
throw new ArgumentException("MultiBrush has no tiles or actors");
}
///
/// Create a new empty MultiBrush with a default weight of 1.0.
///
public MultiBrush()
{
Weight = 1.0f;
tiles = new List<(CVec, TerrainTile)>();
actorPlans = new List();
shape = Array.Empty();
}
MultiBrush(MultiBrush other)
{
Weight = other.Weight;
tiles = new List<(CVec, TerrainTile)>(other.tiles);
actorPlans = new List(other.actorPlans);
shape = other.shape.ToArray();
}
public MultiBrush(Map map, MultiBrushInfo info)
: this()
{
WithWeight(info.Weight);
foreach (var actor in info.Actors)
WithActor(new ActorPlan(map, actor).AlignFootprint());
if (info.BackingTile != null)
WithBackingTile((TerrainTile)info.BackingTile);
foreach (var template in info.Templates)
WithTemplate(map, template);
foreach (var tile in info.Tiles)
WithTile(tile);
}
/// Load a named MultiBrush collection from a map's tileset.
public static ImmutableArray LoadCollection(Map map, string name)
{
var templatedTerrainInfo = map.Rules.TerrainInfo as ITemplatedTerrainInfo;
return templatedTerrainInfo.MultiBrushCollections[name]
.Select(info => new MultiBrush(map, info))
.ToImmutableArray();
}
///
/// Clone the brush. Note that this does not deep clone any ActorPlans.
///
public MultiBrush Clone()
{
return new MultiBrush(this);
}
void UpdateShape()
{
var xys = new HashSet();
foreach (var (xy, _) in tiles)
xys.Add(xy);
foreach (var actorPlan in actorPlans)
foreach (var cpos in actorPlan.Footprint().Keys)
xys.Add(new CVec(cpos.X, cpos.Y));
shape = xys.OrderBy(xy => (xy.Y, xy.X)).ToArray();
}
///
/// Add tiles from a template, optionally with a given offset. By
/// default, it will be auto-offset such that the first tile is
/// under (0, 0).
///
public MultiBrush WithTemplate(Map map, ushort templateId, CVec? offset = null)
{
var tileset = map.Rules.TerrainInfo as ITemplatedTerrainInfo;
if (!tileset.Templates.TryGetValue(templateId, out var templateInfo))
throw new ArgumentException($"Map's tileset does not contain template with ID {templateId}.");
return WithTemplate(templateInfo, offset);
}
public MultiBrush WithTemplate(TerrainTemplateInfo templateInfo, CVec? offset = null)
{
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)
{
if (offset == null)
offset = new CVec(-x, -y);
var tile = new TerrainTile(templateInfo.Id, (byte)i);
tiles.Add((new CVec(x, y) + (CVec)offset, tile));
}
}
UpdateShape();
return this;
}
///
/// Add a single tile, optionally with a given offset. By default, it
/// will be positioned under (0, 0).
///
public MultiBrush WithTile(TerrainTile tile, CVec? offset = null)
{
tiles.Add((offset ?? new CVec(0, 0), tile));
UpdateShape();
return this;
}
/// Add an actor (using the ActorPlan's location as an offset).
public MultiBrush WithActor(ActorPlan actor)
{
actorPlans.Add(actor);
UpdateShape();
return this;
}
///
/// For all spaces occupied by the brush, add the given tile.
/// This is useful for adding a backing tile for actors.
///
public MultiBrush WithBackingTile(TerrainTile tile)
{
if (Area == 0)
throw new InvalidOperationException("No area");
foreach (var xy in shape)
tiles.Add((xy, tile));
return this;
}
/// Update the weight.
public MultiBrush WithWeight(float weight)
{
if (!(weight > 0.0f))
throw new ArgumentException("Weight was not > 0.0");
Weight = weight;
return this;
}
///
/// 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.
/// If nothing could be painted, throws ArgumentException.
///
public void Paint(Map map, List actorPlans, CPos paintAt, Replaceability contract)
{
switch (contract)
{
case Replaceability.None:
throw new ArgumentException("Cannot paint: Replaceability.None");
case Replaceability.Any:
if (this.actorPlans.Count > 0)
PaintActors(map, actorPlans, paintAt);
else if (tiles.Count > 0)
PaintTiles(map, paintAt);
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);
PaintActors(map, actorPlans, paintAt);
break;
case Replaceability.Actor:
if (this.actorPlans.Count == 0)
throw new ArgumentException("Cannot paint: no actors");
PaintActors(map, actorPlans, paintAt);
break;
}
}
void PaintTiles(Map map, CPos paintAt)
{
foreach (var (xy, tile) in tiles)
{
var mpos = (paintAt + xy).ToMPos(map);
if (map.Tiles.Contains(mpos))
map.Tiles[mpos] = tile;
}
}
void PaintActors(Map map, List actorPlans, CPos paintAt)
{
foreach (var actorPlan in this.actorPlans)
{
if (map != actorPlan.Map)
throw new ArgumentException("ActorPlan is for a different map");
var plan = actorPlan.Clone();
var offset = plan.Location;
plan.Location = paintAt + new CVec(offset.X, offset.Y);
actorPlans.Add(plan);
}
}
///
/// Paint an area defined by replace onto map and actorPlans using availableBrushes.
///
public static void PaintArea(
Map map,
List actorPlans,
CellLayer replace,
IReadOnlyList availableBrushes,
MersenneTwister random)
{
var brushesByAreaDict = new Dictionary>();
foreach (var brush in availableBrushes)
{
if (!brushesByAreaDict.ContainsKey(brush.Area))
brushesByAreaDict.Add(brush.Area, new List());
brushesByAreaDict[brush.Area].Add(brush);
}
var brushesByArea = brushesByAreaDict
.OrderBy(kv => -kv.Key)
.ToList();
var brushTotalArea = availableBrushes.Sum(t => t.Area);
var brushTotalWeight = availableBrushes.Sum(t => t.Weight);
// Give 1-by-1 actors the final pass, as they are most flexible.
brushesByArea.Add(
new KeyValuePair>(
1,
availableBrushes.Where(o => o.HasActors && o.Area == 1).ToList()));
var size = map.MapSize;
var replaceMposes = new List();
var remaining = new CellLayer(map);
for (var v = 0; v < size.Y; v++)
for (var u = 0; u < size.X; u++)
{
var mpos = new MPos(u, v);
if (replace[mpos] != Replaceability.None)
{
remaining[mpos] = true;
replaceMposes.Add(mpos);
}
else
{
remaining[mpos] = false;
}
}
var mposes = new MPos[size.X * size.Y];
int mposCount;
void RefreshIndices()
{
mposCount = 0;
foreach (var mpos in replaceMposes)
if (remaining[mpos])
{
mposes[mposCount] = mpos;
mposCount++;
}
random.ShuffleInPlace(mposes, 0, mposCount);
}
Replaceability ReserveShape(CPos paintAt, IEnumerable shape, Replaceability contract)
{
foreach (var cvec in shape)
{
var cpos = paintAt + cvec;
if (!replace.Contains(cpos))
continue;
if (!remaining[cpos])
{
// Can't reserve - not the right shape
return Replaceability.None;
}
contract &= replace[cpos];
if (contract == Replaceability.None)
{
// Can't reserve - obstruction choice doesn't comply
// with replaceability of original tiles.
return Replaceability.None;
}
}
// Can reserve. Commit.
foreach (var cvec in shape)
{
var cpos = paintAt + cvec;
if (!replace.Contains(cpos))
continue;
remaining[cpos] = false;
}
return contract;
}
foreach (var brushesKv in brushesByArea)
{
var brushes = brushesKv.Value;
if (brushes.Count == 0)
continue;
var brushArea = brushes[0].Area;
var brushWeights = brushes.Select(o => o.Weight).ToArray();
var brushWeightForArea = brushWeights.Sum();
var remainingQuota =
brushArea == 1
? int.MaxValue
: (int)Math.Ceiling(replaceMposes.Count * brushWeightForArea / brushTotalWeight);
RefreshIndices();
foreach (var mpos in mposes)
{
var brush = brushes[random.PickWeighted(brushWeights)];
var paintAt = mpos.ToCPos(map);
var contract = ReserveShape(paintAt, brush.Shape, brush.Contract());
if (contract != Replaceability.None)
brush.Paint(map, actorPlans, paintAt, contract);
remainingQuota -= brushArea;
if (remainingQuota <= 0)
break;
}
}
}
}
}