#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 return Replaceability.None; } /// /// 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)); if (xys.Count != 0) shape = xys.OrderBy(xy => (xy.Y, xy.X)).ToArray(); else shape = new[] { new CVec(0, 0) }; } /// /// 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; } } } } }