#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 System.Text.RegularExpressions; using OpenRA.Graphics; using OpenRA.Mods.Common.EditorBrushes; using OpenRA.Mods.Common.Terrain; using OpenRA.Mods.Common.Traits; 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 sealed class ActorInfo { [FieldLoader.Ignore] public readonly string Type; public readonly WVec Offset = WVec.Zero; public ActorInfo(string type) { Type = type; } public ActorInfo(MiniYaml my) { if (string.IsNullOrEmpty(my.Value)) throw new YamlException("Missing actor type"); Type = my.Value; FieldLoader.Load(this, my); } } public sealed class TemplateInfo { [FieldLoader.Ignore] public readonly ushort Type; public readonly CVec Offset = CVec.Zero; public TemplateInfo(ushort type) { Type = type; } public TemplateInfo(MiniYaml my) { if (string.IsNullOrEmpty(my.Value)) throw new YamlException("Missing template type"); if (!Exts.TryParseUshortInvariant(my.Value, out Type)) throw new YamlException($"Invalid MultiBrush Template `${my.Value}`"); FieldLoader.Load(this, my); } } public sealed class TileInfo { [FieldLoader.Ignore] public readonly TerrainTile Type; public readonly CVec Offset = CVec.Zero; public TileInfo(MiniYaml my) { if (string.IsNullOrEmpty(my.Value)) throw new YamlException("Missing tile type"); if (!TerrainTile.TryParse(my.Value, out Type)) throw new YamlException($"Invalid MultiBrush Tile `${my.Value}`"); FieldLoader.Load(this, my); } } public readonly int Weight; public readonly ImmutableArray Actors; public readonly TerrainTile? BackingTile; public readonly ImmutableArray Templates; public readonly ImmutableArray Tiles; public readonly MultiBrushSegment Segment; public MultiBrushInfo( MiniYaml my = null, int weight = MultiBrush.DefaultWeight, IEnumerable actors = null, TerrainTile? backingTile = null, IEnumerable templates = null, IEnumerable tiles = null, MultiBrushSegment segment = null) { Weight = weight; var actorsAcc = (actors ?? []).ToList(); BackingTile = backingTile; var templatesAcc = (templates ?? []).ToList(); var tilesAcc = (tiles ?? []).ToList(); Segment = segment; foreach (var node in my?.Nodes ?? []) switch (node.Key.Split('@')[0]) { case "Weight": if (!Exts.TryParseInt32Invariant(node.Value.Value, out Weight)) throw new YamlException($"Invalid MultiBrush Weight `{node.Value.Value}`"); break; case "Actor": actorsAcc.Add(new ActorInfo(node.Value)); break; case "BackingTile": if (TerrainTile.TryParse(node.Value.Value, out var bt)) BackingTile = bt; else throw new YamlException($"Invalid MultiBrush BackingTile `{node.Value.Value}`"); break; case "Template": templatesAcc.Add(new TemplateInfo(node.Value)); break; case "Tile": tilesAcc.Add(new TileInfo(node.Value)); break; case "Segment": if (Segment != null) throw new YamlException("Multiple MultiBrush Segment definitions"); Segment = new MultiBrushSegment(node.Value); break; default: throw new YamlException($"Unrecognized MultiBrush key {node.Key.Split('@')[0]}"); } Actors = [.. actorsAcc]; Templates = [.. templatesAcc]; Tiles = [.. tilesAcc]; } public static ImmutableArray ParseCollection(MiniYaml my) { var brushes = new List(); foreach (var node in my.Nodes) { switch (node.Key.Split('@')[0]) { case "MultiBrush": brushes.Add(new MultiBrushInfo(node.Value)); break; case "FromTemplates": foreach (var template in FieldLoader.GetValue>(node.Key, node.Value.Value)) brushes.Add(new MultiBrushInfo( my: node.Value, templates: [new TemplateInfo(template)])); break; case "FromActors": foreach (var actor in FieldLoader.GetValue>(node.Key, node.Value.Value)) brushes.Add(new MultiBrushInfo( my: node.Value, actors: [new ActorInfo(actor)])); break; default: throw new YamlException($"Invalid MultiBrush collection key `{node.Key}`"); } } return brushes.ToImmutableArray(); } } /// /// Information about how certain MultiBrushes (like cliffs, beaches, roads) link together. /// public sealed class MultiBrushSegment { /// Start type, including a direction. E.g. "Cliff.R". [FieldLoader.Require] public readonly string Start; /// /// Inner type. Does not include a direction. E.g. "Cliff". /// A null (absent) inner type implies that both the start and end types can be considered /// valid inner types. /// public readonly string Inner = null; /// End type, including a direction. E.g. "Cliff.R". [FieldLoader.Require] public readonly string End; /// /// Point sequence, where points are -X-Y corners of template tiles. /// [FieldLoader.Ignore] public readonly ImmutableArray Points; /// /// Create a Segment from a point sequence and given start, inner, and end types. /// public MultiBrushSegment(string start, string inner, string end, ImmutableArray points) { Start = start; Inner = inner; End = end; Points = points; } public MultiBrushSegment(MiniYaml my) { FieldLoader.Load(this, my); { // Unlike FieldLoader.ParseInt2Array, whitespace is ignored. var value = my.NodeWithKey("Points").Value.Value; var parts = Regex.Replace(value, @"\s+", string.Empty) .Split(',', StringSplitOptions.RemoveEmptyEntries); if (parts.Length % 2 != 0) FieldLoader.InvalidValueAction(value, typeof(int2[]), "Points"); var points = new CVec[parts.Length / 2]; for (var i = 0; i < points.Length; i++) { points[i] = new CVec(Exts.ParseInt32Invariant(parts[2 * i]), Exts.ParseInt32Invariant(parts[2 * i + 1])); if (i > 0) { var step = points[i] - points[i - 1]; if (Math.Abs(step.X) + Math.Abs(step.Y) != 1) throw new YamlException($"Points sequence {value} has non-unit steps"); } } Points = [.. points]; } } public static bool MatchesType(string type, string matcher) { if (type == matcher) return true; return type.StartsWith($"{matcher}.", StringComparison.InvariantCulture); } public bool HasStartType(string matcher) => MatchesType(Start, matcher); public bool HasInnerType(string matcher) => Inner != null ? MatchesType(Inner, matcher) : (MatchesType(Start, matcher) || MatchesType(End, matcher)); public bool HasEndType(string matcher) => MatchesType(End, matcher); public static Direction TypeDirection(string type) { if (!Enum.TryParse(type.Split('.')[^1], out Direction direction)) throw new InvalidOperationException("MultiBrushSegment has invalid direction"); return direction; } public Direction StartDirection => TypeDirection(Start); public Direction EndDirection => TypeDirection(End); } /// A super template that can be used to paint both tiles and actors. public sealed class MultiBrush { public const int DefaultWeight = 1000; 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, } readonly struct TileRange { public readonly ushort Type; public readonly byte MinIndex; public readonly byte MaxIndex; // Height is relative, so can be negative. public readonly short HeightOffset; public readonly byte Ramp; public TileRange(ushort type, byte minIndex, byte maxIndex, short heightOffset, byte ramp) { Type = type; MinIndex = minIndex; MaxIndex = maxIndex; HeightOffset = heightOffset; Ramp = ramp; } public TileRange(ushort type, byte index, short heightOffset, byte ramp) : this(type, index, index, heightOffset, ramp) { } public TileRange(TerrainTile tile, short heightOffset, byte ramp) : this(tile.Type, tile.Index, heightOffset, ramp) { } /// 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)); } /// Create a copy of this TileRange, adding an additional heightOffset. public TileRange WithHeightOffset(short heightOffset) { return new(Type, MinIndex, MaxIndex, (short)(HeightOffset + heightOffset), Ramp); } } public int Weight; readonly List<(CVec XY, TileRange TileRange)> tiles; readonly List actorPlans; public MultiBrushSegment Segment { get; private set; } // A cache for the shape/footprint of the brush. // Null means the shape is dirty and must be recomputed. CVec[] shape; public bool HasTiles => tiles.Count != 0; public bool HasActors => actorPlans.Count != 0; public IEnumerable Shape => GetShape(); /// Total area covered by the MultiBrush. public int Area => GetShape().Length; /// /// The CVec of the first cell covered by the MultiBrush. This is the left-most cell in the /// top-row. Note that this does not necessarily correspond to the top-left corner of the /// rectangular bounds of the MultiBrush. /// public CVec FirstCell => GetShape()[0]; public IEnumerable<(CVec XY, short Height, byte Ramp)> GetHeightsAndRamps() { return tiles.Select(t => (t.XY, t.TileRange.HeightOffset, t.TileRange.Ramp)); } 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 = DefaultWeight; tiles = []; actorPlans = []; Segment = null; shape = null; } MultiBrush(MultiBrush other) { Weight = other.Weight; tiles = [.. other.tiles]; actorPlans = [.. other.actorPlans]; Segment = null; shape = [.. other.shape]; } public MultiBrush(Map map, MultiBrushInfo info) : this() { WithWeight(info.Weight); foreach (var actorInfo in info.Actors) { var actor = new ActorPlan(map, actorInfo.Type) { WPosLocation = WPos.Zero + actorInfo.Offset }; WithActor(actor); } if (info.BackingTile != null) WithBackingTile((TerrainTile)info.BackingTile); foreach (var templateInfo in info.Templates) WithTemplate(map, templateInfo.Type, templateInfo.Offset); foreach (var tileInfo in info.Tiles) WithTile(tileInfo.Type, tileInfo.Offset); ReplaceSegment(info.Segment); } /// Load a named MultiBrush collection from a map's tileset. public static ImmutableArray LoadCollection(Map map, string name) { var templatedTerrainInfo = (ITemplatedTerrainInfo)map.Rules.TerrainInfo; 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 CVec(0, 0)]; } CVec[] GetShape() { if (shape == null) UpdateShape(); return shape; } /// /// 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, short heightOffset = 0) { var itti = (ITemplatedTerrainInfo)map.Rules.TerrainInfo; return WithTemplate(itti, templateId, offset, heightOffset); } public MultiBrush WithTemplate(ITemplatedTerrainInfo itti, ushort templateId, CVec offset, short heightOffset = 0) { if (!itti.Templates.TryGetValue(templateId, out var templateInfo)) throw new ArgumentException($"Tileset does not contain template with ID {templateId}."); return WithTemplate(templateInfo, offset, heightOffset); } public MultiBrush WithTemplate(TerrainTemplateInfo templateInfo, CVec offset, short heightOffset = 0) { if (templateInfo.PickAny) { // Assume that single tiles have equal height. tiles.Add(( offset, new( templateInfo.Id, 0, (byte)(templateInfo.TilesCount - 1), (short)(templateInfo[0].Height + heightOffset), templateInfo[0].RampType))); } else { 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(( new CVec(x, y) + offset, new( templateInfo.Id, (byte)i, (short)(templateInfo[i].Height + heightOffset), templateInfo[i].RampType))); } } shape = null; 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, short heightOffset = 0, byte ramp = 0) { tiles.Add((offset, new(tile, heightOffset, ramp))); shape = null; return this; } /// Add an actor (using the ActorPlan's location as an offset). public MultiBrush WithActor(ActorPlan actor) { actorPlans.Add(actor); shape = null; 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, new(tile, 0, 0))); return this; } /// /// Adds a Segment to this MultiBrush for later use with TilingPath. /// public MultiBrush ReplaceSegment(MultiBrushSegment segment) { Segment = segment; return this; } /// Update the weight. public MultiBrush WithWeight(int weight) { if (weight <= 0) throw new ArgumentException("Weight was not > 0"); Weight = weight; return this; } /// /// Add the tiles and actors from another MultiBrush into this one at a given offset. /// (Does not copy segments.) /// public void MergeFrom(MultiBrush other, CVec at, MapGridType mapGridType, short heightOffset = 0) { foreach (var original in other.actorPlans) { var actorPlan = original.Clone(); actorPlan.WPosLocation += CellLayerUtils.CVecToWVec(at, mapGridType); actorPlans.Add(actorPlan); } foreach (var (xy, tile) in other.tiles) tiles.Add((xy + at, tile.WithHeightOffset(heightOffset))); shape = null; } /// /// Paint tiles onto the map and/or add actors to actorPlans at the given location. /// A specific height offset can be supplied, else one will be assumed from the map. /// 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, short? heightOffset, Replaceability contract, MersenneTwister random) { short finalHeightOffset = 0; if (heightOffset.HasValue) { finalHeightOffset = heightOffset.Value; } else { foreach (var cpos in Shape) { if (map.Height.Contains(paintAt + cpos)) { finalHeightOffset = map.Height[paintAt + cpos]; break; } } } switch (contract) { case Replaceability.None: throw new ArgumentException("Cannot paint: Replaceability.None"); case Replaceability.Any: PaintTiles(map, paintAt, finalHeightOffset, random); PaintActors(map, actorPlans, paintAt); break; case Replaceability.Tile: if (tiles.Count == 0) throw new ArgumentException("Cannot paint: no tiles"); PaintTiles(map, paintAt, finalHeightOffset, random); 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, short heightOffset, MersenneTwister random) { foreach (var (xy, tile) in tiles) { var mpos = (paintAt + xy).ToMPos(map); if (map.Tiles.Contains(mpos)) { // map.Ramp does not need to be updated here. map.Tiles[mpos] = tile.Pick(random); map.Height[mpos] = (byte)Math.Clamp(tile.HeightOffset + heightOffset, byte.MinValue, byte.MaxValue); } } } 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, bool alwaysPreferLargerBrushes = false, short? heightOffset = null) { var brushesByAreaDict = new Dictionary>(); foreach (var brush in availableBrushes) { if (!brushesByAreaDict.ContainsKey(brush.Area)) brushesByAreaDict.Add(brush.Area, []); 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.Height; v++) { for (var u = 0; u < size.Width; 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.Width * size.Height]; int mposCount; void RefreshIndices() { mposCount = 0; foreach (var mpos in replaceMposes) if (remaining[mpos]) { mposes[mposCount] = mpos; mposCount++; } random.ShuffleInPlace(mposes.AsSpan(), 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 || alwaysPreferLargerBrushes) ? int.MaxValue : (int)(((long)replaceMposes.Count * brushWeightForArea + brushTotalWeight - 1) / brushTotalWeight); RefreshIndices(); foreach (var mpos in mposes) { var brush = brushes[random.PickWeighted(brushWeights)]; var paintAt = mpos.ToCPos(map) - brush.FirstCell; var contract = ReserveShape(paintAt, brush.Shape, brush.Contract()); if (contract != Replaceability.None) brush.Paint(map, actorPlans, paintAt, heightOffset, contract, random); remainingQuota -= brushArea; if (remainingQuota <= 0) break; } } } /// /// Create a sparse EditorBlitSource from this MultiBrush. The EditorBlitSource will have /// 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, short heightOffset = 0) { var world = worldRenderer.World; var map = world.Map; if (defaultActorOwner == null) { var editorActorLayer = world.WorldActor.Trait(); if (editorActorLayer != null) defaultActorOwner = editorActorLayer.Players.Players.Values.First(); } var players = world.Players.ToDictionary( player => player.InternalName, player => player.PlayerReference); var topLeft = new CPos( Shape.Min(cvec => cvec.X), Shape.Min(cvec => cvec.Y)); var bottomRight = new CPos( Shape.Max(cvec => cvec.X), Shape.Max(cvec => cvec.Y)); var cellRegion = new CellCoordsRegion(topLeft, bottomRight); var actorPreviews = new Dictionary(); for (var i = 0; i < actorPlans.Count; i++) { // A (non-revert) EditorBlitSource's actors' names are generally unimportant beyond // needing to be distinct. They will get renamed when blitting. var name = $"Actor{i}"; var actorReference = actorPlans[i].Reference.Clone(); var ownerInit = actorReference.Get(); if (!players.TryGetValue(ownerInit.InternalName, out var owner)) owner = defaultActorOwner; if (owner == null) throw new InvalidOperationException("MultiBrush actor has invalid (or no) owner and no default available."); actorPreviews[name] = new EditorActorPreview( worldRenderer, name, actorReference, owner); } var blitTiles = tiles .Where(t => map.Tiles.Contains(CPos.Zero + t.XY)) .DistinctBy(t => t.XY) .Select(t => (t.XY, Tile: t.TileRange.Pick(random), t.TileRange.HeightOffset)) .ToDictionary( t => CPos.Zero + t.XY, t => new BlitTile(t.Tile, default, null, (byte)Math.Clamp(heightOffset + t.HeightOffset, byte.MinValue, byte.MaxValue))); return new EditorBlitSource( cellRegion, 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; } } }