Add experimental RA procedural map generator

Add an experimental procedural map generator for the Red Alert mod,
along with supporting code that may assist in the development of map
generators for other mods.

Map generation may be accessed as a tool in the Map Editor. This change
does not presently introduce direct lobby options for generated maps.

Features:

- Terrain with land, water, beaches, cliffs, roads, debris, and trees.
- Placement of mpspawns, neutral buildings, and resources.
- Rotational and mirror symmetry options.
- Various configurable parameters with presets.
- Deterministic with configurable seed.
- Performant.
This commit is contained in:
Ashley Newson
2025-01-07 22:48:08 +00:00
committed by Gustas
parent 0536c58b78
commit 417f787294
41 changed files with 11795 additions and 7 deletions

View File

@@ -36,6 +36,7 @@ Also thanks to:
* Andreas Beck (baxtor)
* Ang Soon Li (asl97)
* Arik Lirette (Angusm3)
* Ashley Newson
* Barnaby Smith (mvi)
* Bellator
* Bernd Stellwag (burned42)

View File

@@ -510,6 +510,11 @@ namespace OpenRA
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static ushort ParseUshortInvariant(string s)
{
return ushort.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static short ParseInt16Invariant(string s)
{
return short.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
@@ -520,6 +525,22 @@ namespace OpenRA
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static float ParseFloatOrPercentInvariant(string s)
{
var f = float.Parse(s.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo);
return f * (s.Contains('%') ? 0.01f : 1f);
}
public static bool TryParseByteInvariant(string s, out byte i)
{
return byte.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
public static bool TryParseUshortInvariant(string s, out ushort i)
{
return ushort.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
public static bool TryParseInt32Invariant(string s, out int i)
{
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
@@ -530,6 +551,17 @@ namespace OpenRA
return long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
public static bool TryParseFloatOrPercentInvariant(string s, out float f)
{
if (float.TryParse(s.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out f))
{
f *= s.Contains('%') ? 0.01f : 1f;
return true;
}
return false;
}
public static string ToStringInvariant(this byte i)
{
return i.ToString(NumberFormatInfo.InvariantInfo);
@@ -545,6 +577,11 @@ namespace OpenRA
return i.ToString(NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this float f)
{
return f.ToString(NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this int i, string format)
{
return i.ToString(format, NumberFormatInfo.InvariantInfo);

View File

@@ -63,12 +63,16 @@ namespace OpenRA
var u = (x - y) / 2;
var v = x + y;
if (!Bounds.Contains(u, v))
throw new IndexOutOfRangeException();
return v * Size.Width + u;
}
// Resolve an array index from map coordinates
int Index(MPos uv)
{
if (!Bounds.Contains(uv.U, uv.V))
throw new IndexOutOfRangeException();
return uv.V * Size.Width + uv.U;
}
@@ -145,6 +149,9 @@ namespace OpenRA
{
return uv.Clamp(new Rectangle(0, 0, Size.Width - 1, Size.Height - 1));
}
public CellRegion CellRegion =>
new(GridType, new MPos(0, 0), new MPos(Size.Width - 1, Size.Height - 1));
}
// Helper functions

View File

@@ -1145,6 +1145,11 @@ namespace OpenRA
}
public byte GetTerrainIndex(CPos cell)
{
return GetTerrainIndex(cell.ToMPos(this));
}
public byte GetTerrainIndex(MPos uv)
{
// Lazily initialize a cache for terrain indexes.
if (cachedTerrainIndexes == null)
@@ -1153,7 +1158,6 @@ namespace OpenRA
cachedTerrainIndexes.Clear(InvalidCachedTerrainIndex);
}
var uv = cell.ToMPos(this);
var terrainIndex = cachedTerrainIndexes[uv];
// PERF: Cache terrain indexes per cell on demand.
@@ -1168,7 +1172,12 @@ namespace OpenRA
public TerrainTypeInfo GetTerrainInfo(CPos cell)
{
return Rules.TerrainInfo.TerrainTypes[GetTerrainIndex(cell)];
return GetTerrainInfo(cell.ToMPos(this));
}
public TerrainTypeInfo GetTerrainInfo(MPos uv)
{
return Rules.TerrainInfo.TerrainTypes[GetTerrainIndex(uv)];
}
public CPos Clamp(CPos cell)

View File

@@ -25,6 +25,21 @@ namespace OpenRA
public override int GetHashCode() { return Type.GetHashCode() ^ Index.GetHashCode(); }
public override string ToString() { return Type + "," + Index; }
public static bool TryParse(string s, out TerrainTile tt)
{
var split = s.Split(',');
if (split.Length == 2 &&
Exts.TryParseUshortInvariant(split[0], out var type) &&
Exts.TryParseByteInvariant(split[1], out var index))
{
tt = new TerrainTile(type, index);
return true;
}
tt = default;
return false;
}
}
public readonly struct ResourceTile

View File

@@ -0,0 +1,130 @@
#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;
namespace OpenRA.Primitives
{
/// <summary>
/// A random-access array which keeps track of the minimum item.
/// </summary>
public sealed class PriorityArray<T> where T : IComparable<T>
{
readonly T[] items;
readonly int[] itemIndexToHeapIndex;
readonly int[] heapOfItemIndices;
/// <summary>
/// Create a new PriorityArray of given size with all values preset to the given init value.
/// </summary>
public PriorityArray(int size, T init)
{
items = new T[size];
itemIndexToHeapIndex = new int[size];
heapOfItemIndices = new int[size];
Array.Fill(items, init);
for (var i = 0; i < size; i++)
{
items[i] = init;
itemIndexToHeapIndex[i] = i;
heapOfItemIndices[i] = i;
}
}
public int Length => items.Length;
/// <summary>Get the index of the minimum element.</summary>
public int GetMinIndex() => heapOfItemIndices[0];
public T this[int itemIndex]
{
get => items[itemIndex];
set
{
items[itemIndex] = value;
Bubble(itemIndexToHeapIndex[itemIndex]);
}
}
void Bubble(int heapIndex)
{
if (!BubbleUp(heapIndex))
{
BubbleDown(heapIndex);
}
}
bool BubbleUp(int heapIndex)
{
if (heapIndex == 0)
return false;
var itemIndex = heapOfItemIndices[heapIndex];
var upHeapIndex = ((heapIndex + 1) >> 1) - 1;
var upItemIndex = heapOfItemIndices[upHeapIndex];
if (items[itemIndex].CompareTo(items[upItemIndex]) >= 0)
return false;
Swap(heapIndex, upHeapIndex, itemIndex, upItemIndex);
BubbleUp(upHeapIndex);
return true;
}
bool BubbleDown(int heapIndex)
{
var itemIndex = heapOfItemIndices[heapIndex];
var leftDownHeapIndex = ((heapIndex + 1) << 1) - 1;
var rightDownHeapIndex = leftDownHeapIndex + 1;
if (leftDownHeapIndex >= Length)
return false;
var item = items[itemIndex];
if (rightDownHeapIndex < Length)
{
var leftDownItemIndex = heapOfItemIndices[leftDownHeapIndex];
var rightDownItemIndex = heapOfItemIndices[rightDownHeapIndex];
var leftDownItem = items[leftDownItemIndex];
var rightDownItem = items[rightDownItemIndex];
if (item.CompareTo(leftDownItem) <= 0 && item.CompareTo(rightDownItem) <= 0)
return false;
if (leftDownItem.CompareTo(rightDownItem) <= 0)
{
Swap(heapIndex, leftDownHeapIndex, itemIndex, leftDownItemIndex);
BubbleDown(leftDownHeapIndex);
}
else
{
Swap(heapIndex, rightDownHeapIndex, itemIndex, rightDownItemIndex);
BubbleDown(rightDownHeapIndex);
}
return true;
}
else
{
// Just the left down one exists.
var leftDownItemIndex = heapOfItemIndices[leftDownHeapIndex];
var leftDownItem = items[leftDownItemIndex];
if (item.CompareTo(leftDownItem) <= 0)
return false;
Swap(heapIndex, leftDownHeapIndex, itemIndex, leftDownItemIndex);
BubbleDown(leftDownHeapIndex);
return true;
}
}
void Swap(int heapIndex1, int heapIndex2, int itemIndex1, int itemIndex2)
{
(itemIndexToHeapIndex[itemIndex1], itemIndexToHeapIndex[itemIndex2]) =
(itemIndexToHeapIndex[itemIndex2], itemIndexToHeapIndex[itemIndex1]);
(heapOfItemIndices[heapIndex1], heapOfItemIndices[heapIndex2]) =
(heapOfItemIndices[heapIndex2], heapOfItemIndices[heapIndex1]);
}
}
}

View File

@@ -46,6 +46,16 @@ namespace OpenRA.Primitives
public bool IsEmpty => Width == 0 && Height == 0;
public int2 ToInt2()
{
return new(Width, Height);
}
public static Size FromInt2(int2 xy)
{
return new(xy.X, xy.Y);
}
public bool Equals(Size other)
{
return this == other;

View File

@@ -10,6 +10,8 @@
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
namespace OpenRA.Support
{
@@ -32,7 +34,10 @@ namespace OpenRA.Support
mt[i] = 1812433253u * (mt[i - 1] ^ (mt[i - 1] >> 30)) + i;
}
public int Next()
/// <summary>
/// Produces an unsigned integer between -0x80000000 and 0x7fffffff inclusive.
/// </summary>
public uint NextUint()
{
if (index == 0) Generate();
@@ -45,6 +50,24 @@ namespace OpenRA.Support
index = (index + 1) % 624;
TotalCount++;
Last = (int)(y % int.MaxValue);
return y;
}
/// <summary>
/// Produces an unsigned integer between -0x80000000 and 0x7fffffff inclusive.
/// </summary>
public ulong NextUlong()
{
return (ulong)NextUint() << 32 | NextUint();
}
/// <summary>
/// Produces signed integers between -0x7fffffff and 0x7fffffff inclusive.
/// 0 is twice as likely as any other number.
/// </summary>
public int Next()
{
NextUint();
return Last;
}
@@ -65,11 +88,73 @@ namespace OpenRA.Support
return Next(0, high);
}
/// <summary>
/// Produces random 32-bit floats between 0 inclusive and 1 inclusive.
/// Note that whilst floats are 32-bit (23-bit mantissa), the entropy is not. Lower numbers preserve more entropy.
/// </summary>
public float NextFloat()
{
return Math.Abs(Next() / (float)0x7fffffff);
}
/// <summary>
/// Produces uniformally distributed random floats between 0 inclusive and 1 exclusive.
/// Note that whilst floats are 32-bit (23-bit mantissa), each output contains exactly 23 bits of entropy.
/// </summary>
public float NextFloatExclusive()
{
return (NextUint() & 0x7fffff) / (float)0x800000;
}
/// <summary>
/// Produces uniformally distributed random doubles between 0 inclusive and 1 exclusive.
/// Note that whilst doubles are 64-bit (52-bit mantissa), each output contains exactly 52 bits of entropy.
/// </summary>
public double NextDoubleExclusive()
{
return (NextUlong() & 0xfffffffffffffL) / (double)0x10000000000000L;
}
/// <summary>
/// Pick a random an index from a list of weights.
/// </summary>
public int PickWeighted(IReadOnlyList<float> weights)
{
var total = weights.Sum();
var spin = NextFloatExclusive() * total;
int i;
float acc = 0;
for (i = 0; i < weights.Count; i++)
{
acc += weights[i];
if (spin < acc)
return i;
}
// This might be possible due to floating point precision loss
// (in rare cases). Or we might have been given rubbish
// weights. Return anything > 0.
for (i = 0; i < weights.Count; i++)
if (weights[i] > 0)
return i;
// All <= 0!
return Next(0, weights.Count);
}
/// <summary>
/// Shuffle a portion of a list list in place. Has minor biases.
/// </summary>
public void ShuffleInPlace<T>(IList<T> list, int start, int len)
{
for (var i = len; i > 1; i--)
{
var swap = Next(i);
(list[start + i - 1], list[start + swap]) =
(list[start + swap], list[start + i - 1]);
}
}
void Generate()
{
unchecked

View File

@@ -0,0 +1,51 @@
#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 OpenRA.Mods.Common.MapGenerator;
using OpenRA.Mods.Common.Terrain;
namespace OpenRA.Mods.Common.Lint
{
public class CheckMultiBrushes : ILintPass
{
public void Run(Action<string> emitError, Action<string> emitWarning, ModData modData)
{
foreach (var (terrainInfoName, terrainInfo) in modData.DefaultTerrainInfo)
{
if (terrainInfo is ITemplatedTerrainInfo templatedTerrainInfo && templatedTerrainInfo.MultiBrushCollections.Count > 0)
{
var map = new Map(modData, terrainInfo, 1, 1);
foreach (var (collectionName, collection) in templatedTerrainInfo.MultiBrushCollections)
foreach (var info in collection)
{
try
{
// Includes validation of actor types and template IDs.
var multiBrush = new MultiBrush(map, info);
// Validates there is at least something in the MultiBrush.
multiBrush.Contract();
foreach (var (_, tile) in multiBrush.Tiles)
if (!templatedTerrainInfo.TryGetTerrainInfo(tile, out var _))
emitError($"Tileset {terrainInfoName} has invalid MultiBrush collection `{collectionName}`: tileset does not have tile {tile.Type},{tile.Index}");
}
catch (Exception e) when (e is ArgumentException || e is InvalidOperationException)
{
emitError($"Tileset {terrainInfoName} has invalid MultiBrush collection `{collectionName}`: {e.Message}");
}
}
}
}
}
}
}

View File

@@ -0,0 +1,149 @@
#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.Linq;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.MapGenerator
{
/// <summary>Description of an actor to add to a map.</summary>
public sealed class ActorPlan
{
public readonly Map Map;
public readonly ActorInfo Info;
public readonly ActorReference Reference;
public CPos Location
{
get => Reference.Get<LocationInit>().Value;
set
{
Reference.RemoveAll<LocationInit>();
Reference.Add(new LocationInit(value));
}
}
public WPos WPosLocation
{
get => CellLayerUtils.CPosToWPos(Location, Map.Grid.Type);
set => Location = CellLayerUtils.WPosToCPos(value, Map.Grid.Type);
}
/// <summary>
/// WPos representation of actor's center.
/// For example, A 1x2 actor on a Rectangular grid will have +WVec(0, 512, 0)
/// offset to its WPosLocation.
/// </summary>
public WPos WPosCenterLocation
{
get => WPosLocation + WVecCenterOffset();
set => WPosLocation = value - WVecCenterOffset();
}
/// <summary>
/// Create an ActorPlan from a reference. The referenced actor becomes owned.
/// </summary>
public ActorPlan(Map map, ActorReference reference)
{
Map = map;
Reference = reference;
if (!map.Rules.Actors.TryGetValue(Reference.Type.ToLowerInvariant(), out Info))
throw new ArgumentException($"MultiBrush Actor of unknown type `{Reference.Type.ToLowerInvariant()}`");
}
/// <summary>
/// Create an ActorPlan containing a new Neutral-owned actor of the given type.
/// </summary>
public ActorPlan(Map map, string type)
: this(map, ActorFromType(type))
{ }
/// <summary>
/// Create a cloned actor plan, cloning the underlying ActorReference.
/// </summary>
public ActorPlan Clone()
{
return new ActorPlan(Map, Reference.Clone());
}
static ActorReference ActorFromType(string type)
{
return new ActorReference(type)
{
new LocationInit(default),
new OwnerInit("Neutral"),
};
}
/// <summary>
/// The footprint of the actor (influenced by its location).
/// </summary>
public IReadOnlyDictionary<CPos, SubCell> Footprint()
{
var location = Location;
var ios = Info.TraitInfoOrDefault<IOccupySpaceInfo>();
var subCellInit = Reference.GetOrDefault<SubCellInit>();
var subCell = subCellInit != null ? subCellInit.Value : SubCell.Any;
var occupiedCells = ios?.OccupiedCells(Info, location, subCell);
if (occupiedCells == null || occupiedCells.Count == 0)
return new Dictionary<CPos, SubCell>() { { location, SubCell.FullCell } };
else
return occupiedCells;
}
/// <summary>
/// Relocates the actor such that the top-most, left-most footprint
/// square is at (0, 0).
/// </summary>
public ActorPlan AlignFootprint()
{
var footprint = Footprint();
var first = footprint.Select(kv => kv.Key).OrderBy(cpos => (cpos.Y, cpos.X)).First();
Location -= new CVec(first.X, first.Y);
return this;
}
/// <summary>
/// <para>
/// Return a WVec center offset (from its WPosLocation) for the actor.
/// </para>
/// <para>
/// For example, for a 1x2 actor on a Rectangular grid, this would be WVec(0, 512, 0).
/// </para>
/// </summary>
WVec WVecCenterOffset()
{
var bi = Info.TraitInfoOrDefault<BuildingInfo>();
if (bi == null)
return new WVec(0, 0, 0);
var left = int.MaxValue;
var right = int.MinValue;
var top = int.MaxValue;
var bottom = int.MinValue;
foreach (var (cvec, type) in bi.Footprint)
{
if (type == FootprintCellType.Empty)
continue;
left = Math.Min(left, cvec.X);
top = Math.Min(top, cvec.Y);
right = Math.Max(right, cvec.X);
bottom = Math.Max(bottom, cvec.Y);
}
return CellLayerUtils.CVecToWVec(new CVec(left + right, top + bottom), Map.Grid.Type) / 2;
}
}
}

View File

@@ -0,0 +1,500 @@
#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.Primitives;
using OpenRA.Support;
namespace OpenRA.Mods.Common.MapGenerator
{
public static class CellLayerUtils
{
static int FloorDiv(int a, int b)
{
var q = Math.DivRem(a, b, out var r);
if (r < 0)
return q - 1;
else
return q;
}
/// <summary>Return true iff a and b have the same grid type and size.</summary>
public static bool AreSameShape<T, U>(CellLayer<T> a, CellLayer<U> b)
{
return a.Size == b.Size && a.GridType == b.GridType;
}
/// <summary>
/// Returns the half-way point between the centers of the top-left (first) and bottom-right
/// (last) MPos cells. This will either lie in the exact center of a cell, an edge between
/// two cells, or a corner between four cells. Note that this might not fit all reasonable
/// or intuitive definitions of a map center, but has convenient properties.
/// </summary>
public static WPos Center<T>(CellLayer<T> cellLayer)
{
switch (cellLayer.GridType)
{
case MapGridType.Rectangular:
return new WPos(
cellLayer.Size.Width * 512,
cellLayer.Size.Height * 512,
0);
case MapGridType.RectangularIsometric:
return new WPos(
(cellLayer.Size.Width * 2 + (~cellLayer.Size.Height & 1)) * 362,
(cellLayer.Size.Height + 1) * 362,
0);
default:
throw new NotImplementedException();
}
}
/// <summary>Get the WPos of the -X-Y corner of a CPos cell.</summary>
public static WPos CornerToWPos(CPos xy, MapGridType gridType)
{
switch (gridType)
{
case MapGridType.Rectangular:
return new WPos(
xy.X * 1024,
xy.Y * 1024,
0);
case MapGridType.RectangularIsometric:
return new WPos(
(xy.X - xy.Y) * 724 + 724,
(xy.X + xy.Y) * 724,
0);
default:
throw new NotImplementedException();
}
}
/// <summary>Get the WVec representing the same translation as the given CVec.</summary>
public static WVec CVecToWVec(CVec cvec, MapGridType gridType)
{
switch (gridType)
{
case MapGridType.Rectangular:
return new WVec(
cvec.X * 1024,
cvec.Y * 1024,
0);
case MapGridType.RectangularIsometric:
return new WVec(
(cvec.X - cvec.Y) * 724,
(cvec.X + cvec.Y) * 724,
0);
default:
throw new NotImplementedException();
}
}
/// <summary>Get the WPos center of a CPos cell.</summary>
public static WPos CPosToWPos(CPos cpos, MapGridType gridType)
{
var wvec = CVecToWVec(new CVec(cpos.X, cpos.Y), gridType);
switch (gridType)
{
case MapGridType.Rectangular:
return new WPos(512, 512, 0) + wvec;
case MapGridType.RectangularIsometric:
return new WPos(724, 724, 0) + wvec;
default:
throw new NotImplementedException();
}
}
/// <summary>
/// Find the CPos cell in which a WPos position lies. WPos positions on
/// an edge or corner match the CPos with higher X and/or Y positions.
/// </summary>
public static CPos WPosToCPos(WPos wpos, MapGridType gridType)
{
switch (gridType)
{
case MapGridType.Rectangular:
return new CPos(
FloorDiv(wpos.X, 1024),
FloorDiv(wpos.Y, 1024),
0);
case MapGridType.RectangularIsometric:
return new CPos(
FloorDiv(wpos.Y + wpos.X - 724, 1448),
FloorDiv(wpos.Y - wpos.X + 724, 1448),
0);
default:
throw new NotImplementedException();
}
}
/// <summary>Get the WPos center of an MPos cell.</summary>
public static WPos MPosToWPos(MPos mpos, MapGridType gridType)
{
return CPosToWPos(mpos.ToCPos(gridType), gridType);
}
/// <summary>
/// Find the MPos cell in which a WPos position lies. WPos positions on
/// an edge or corner match the CPos (not necessarily MPos) with higher
/// X and/or Y positions.
/// </summary>
public static MPos WPosToMPos(WPos wpos, MapGridType gridType)
{
return WPosToCPos(wpos, gridType).ToMPos(gridType);
}
/// <summary>
/// Translates CPos-like positions to zero-based positions.
/// </summary>
public static int2[][] ToMatrixPoints<T>(
IEnumerable<CPos[]> pointArrayArray, CellLayer<T> cellLayer)
{
var cellBounds = CellBounds(cellLayer);
return pointArrayArray
.Select(xys => xys
.Select(xy => new int2(xy.X - cellBounds.Left, xy.Y - cellBounds.Top))
.ToArray())
.ToArray();
}
/// <summary>
/// Translates zero-based positions to CPos-like positions.
/// </summary>
public static CPos[][] FromMatrixPoints<T>(
IEnumerable<int2[]> pointArrayArray, CellLayer<T> cellLayer)
{
var cellBounds = CellBounds(cellLayer);
return pointArrayArray
.Select(xys => xys
.Select(xy => new CPos(xy.X + cellBounds.Left, xy.Y + cellBounds.Top))
.ToArray())
.ToArray();
}
/// <summary>
/// <para>
/// Run an action over the inside or outside of a circle of given center and radius in
/// world coordinates. The action is called with cells' MPos, CPos, WPos center, and the
/// squared distance to the WPos center from the circle's center.
/// If outside is true, the action is run for cells outside of the circle instead of the
/// inside.
/// </para>
/// <para>
/// A cell is inside the circle if its center is &lt;= wRadius from wCenter.
/// Coordinates outside of the CellLayer are ignored.
/// </para>
/// </summary>
public static void OverCircle<T>(
CellLayer<T> cellLayer,
WPos wCenter,
int wRadius,
bool outside,
Action<MPos, CPos, WPos, long> action)
{
var gridType = cellLayer.GridType;
int minU;
int minV;
int maxU;
int maxV;
if (outside)
{
minU = 0;
minV = 0;
maxU = cellLayer.Size.Width - 1;
maxV = cellLayer.Size.Height - 1;
}
else
{
var mCenter = WPosToMPos(wCenter, gridType);
var mRadiusU = wRadius / 1448 + 2;
var mRadiusV = wRadius / 724 + 1;
minU = Math.Max(mCenter.U - mRadiusU, 0);
minV = Math.Max(mCenter.V - mRadiusV, 0);
maxU = Math.Min(mCenter.U + mRadiusU, cellLayer.Size.Width - 1);
maxV = Math.Min(mCenter.V + mRadiusV, cellLayer.Size.Height - 1);
}
var wRadiusSquared = (long)wRadius * wRadius;
for (var v = minV; v <= maxV; v++)
for (var u = minU; u <= maxU; u++)
{
var mpos = new MPos(u, v);
var cpos = mpos.ToCPos(gridType);
var wpos = CPosToWPos(cpos, gridType);
var offset = wCenter - wpos;
var thisRadiusSquared = offset.LengthSquared;
if (thisRadiusSquared <= wRadiusSquared != outside)
action(mpos, cpos, wpos, thisRadiusSquared);
}
}
/// <summary>
/// Return a linear copy of all entries in a CellLayer, ordered v * width + u, similar to
/// MPos(0, 0), MPos(1, 0), MPos(2, 0), ..., MPos(0, 1), MPos(1, 1), MPos(2, 1), ...
/// </summary>
public static T[] Entries<T>(CellLayer<T> cellLayer)
{
var i = 0;
var entries = new T[cellLayer.Size.Width * cellLayer.Size.Height];
foreach (var value in cellLayer)
entries[i++] = value;
return entries;
}
/// <summary>
/// Uniformally add to or subtract from all matrix cells such that the given quantile,
/// fraction, has the given target value.
/// </summary>
public static void CalibrateQuantileInPlace(CellLayer<float> cellLayer, float target, float fraction)
{
var sorted = Entries(cellLayer);
Array.Sort(sorted);
var adjustment = target - MatrixUtils.ArrayQuantile(sorted, fraction);
foreach (var mpos in cellLayer.CellRegion.MapCoords)
cellLayer[mpos] += adjustment;
}
/// <summary>
/// Get the smallest CPos rectangle that contains all cells for the specified grid.
/// </summary>
public static Rectangle CellBounds(Size size, MapGridType gridType)
{
switch (gridType)
{
case MapGridType.Rectangular:
return new Rectangle(0, 0, size.Width, size.Height);
case MapGridType.RectangularIsometric:
{
var maxCX =
new MPos(size.Width - 1, size.Height - 1)
.ToCPos(gridType).X;
var minCY =
new MPos(size.Width - 1, 0)
.ToCPos(gridType).Y;
var maxCY =
new MPos(0, size.Height - 1)
.ToCPos(gridType).Y;
return Rectangle.FromLTRB(0, minCY, maxCX + 1, maxCY + 1);
}
default:
throw new NotImplementedException();
}
}
/// <summary>
/// Get the smallest CPos rectangle that contains all cells in a CellLayer.
/// </summary>
public static Rectangle CellBounds<T>(CellLayer<T> cellLayer)
{
return CellBounds(cellLayer.Size, cellLayer.GridType);
}
/// <summary>
/// Get the smallest CPos rectangle that contains all cells in a map.
/// </summary>
public static Rectangle CellBounds(Map map)
{
return CellBounds(new Size(map.MapSize.X, map.MapSize.Y), map.Grid.Type);
}
/// <summary>
/// Copies a CPos-aligned Matrix into a CellLayer. Depending on the grid type, this may
/// discard data for cells that don't exist in the CellLayer.
/// </summary>
public static void FromMatrix<T>(
CellLayer<T> cellLayer,
Matrix<T> matrix,
bool allowOversizedMatrix = false)
{
var cellBounds = CellBounds(cellLayer);
var size = cellBounds.Size.ToInt2();
if (allowOversizedMatrix)
{
if (matrix.Size.X < size.X || matrix.Size.Y < size.Y)
throw new ArgumentException("source Matrix does not cover destination CellLayer");
}
else if (matrix.Size != size)
{
throw new ArgumentException("destination and source have incompatible sizes");
}
foreach (var cpos in cellLayer.CellRegion)
cellLayer[cpos] = matrix[cpos.X - cellBounds.Left, cpos.Y - cellBounds.Top];
}
/// <summary>
/// Copies a CellLayer into a CPos-aligned Matrix. Depending on the grid type, this may
/// fill the matrix with some default values for cells that don't exist in the CellLayer.
/// </summary>
public static Matrix<T> ToMatrix<T>(CellLayer<T> cellLayer, T defaultValue)
{
var cellBounds = CellBounds(cellLayer);
var matrix = new Matrix<T>(cellBounds.Size.ToInt2()).Fill(defaultValue);
foreach (var cpos in cellLayer.CellRegion)
matrix[cpos.X - cellBounds.Left, cpos.Y - cellBounds.Top] = cellLayer[cpos];
return matrix;
}
/// <summary>Wrapper around MatrixUtils.BordersToPoints in CPos space.</summary>
public static CPos[][] BordersToPoints(CellLayer<bool> cellLayer, CellLayer<bool> mask = null)
{
if (mask != null)
{
if (!AreSameShape(cellLayer, mask))
throw new ArgumentException("cellLayer and mask must have same shape.");
}
else
{
mask = new CellLayer<bool>(cellLayer.GridType, cellLayer.Size);
mask.Clear(true);
}
var matrix = ToMatrix(cellLayer, false);
var maskMatrix = ToMatrix(mask, false);
var matrixPoints = MatrixUtils.BordersToPoints(matrix, maskMatrix);
return FromMatrixPoints(matrixPoints, cellLayer);
}
/// <summary>Wrapper around MatrixUtils.ChebyshevRoom in CPos space.</summary>
public static void ChebyshevRoom(
CellLayer<int> output,
CellLayer<bool> input,
bool outsideValue)
{
var matrix = ToMatrix(input, outsideValue);
var roominess = MatrixUtils.ChebyshevRoom(matrix, outsideValue);
FromMatrix(output, roominess);
}
/// <summary>Wrapper around MatrixUtils.WalkingDistance in CPos space.</summary>
public static void WalkingDistances(
CellLayer<float> distances,
CellLayer<bool> passable,
IEnumerable<CPos> seeds,
float maxDistance)
{
var passableMatrix = ToMatrix(passable, false);
var cellBounds = CellBounds(passable);
var int2Seeds = seeds
.Select(cpos => new int2(cpos.X - cellBounds.Left, cpos.Y - cellBounds.Top));
var distancesMatrix = MatrixUtils.WalkingDistances(passableMatrix, int2Seeds, maxDistance);
FromMatrix(distances, distancesMatrix);
}
/// <summary>
/// Rank all cell values and select the best (greatest compared) value.
/// If there are equally good best candidates, choose one at random.
/// </summary>
public static (MPos MPos, T Value) FindRandomBest<T>(
CellLayer<T> cellLayer,
MersenneTwister random,
Comparison<T> comparison)
{
var candidates = new List<MPos>();
var best = cellLayer[new MPos(0, 0)];
foreach (var mpos in cellLayer.CellRegion.MapCoords)
{
var rank = comparison(cellLayer[mpos], best);
if (rank > 0)
{
best = cellLayer[mpos];
candidates.Clear();
}
if (rank >= 0)
candidates.Add(mpos);
}
var choice = candidates[random.Next(candidates.Count)];
return (choice, best);
}
/// <summary>
/// Pick a random MPos position in a CellLayer where each cell is a
/// selection weight.
/// </summary>
public static MPos PickWeighted(CellLayer<float> weights, MersenneTwister random)
{
var entries = Entries(weights);
var choice = random.PickWeighted(entries);
var v = Math.DivRem(choice, weights.Size.Width, out var u);
return new MPos(u, v);
}
/// <summary>
/// <para>
/// Perform a generic flood fill starting at seeds <c>[(cpos, prop), ...]</c>.
/// </para>
/// <para>
/// For each point being considered for fill, <c>filler(cpos, prop)</c> is
/// called with the current position (cpos) and propagation value (prop).
/// filler should return the value to be propagated or null if not to be
/// propagated. Propagation happens to all neighbours (offsets) defined
/// by spread, regardless of whether they have previously been visited,
/// so filler is responsible for terminating propagation by returning
/// nulls. Usually, <c>Direction.Spread4CVec</c> or <c>Direction.Spread8CVec</c>
/// is appropriate as a spread pattern.
/// </para>
/// <para>
/// filler should capture and manipulate any necessary input and output
/// arrays.
/// </para>
/// <para>
/// Each call to filler will have either an equal or greater
/// growth/propagation distance from their seed value than all calls
/// before it. (You can think of this as them being called in ordered
/// growth layers.)
/// </para>
/// <para>
/// Note that filler may be called multiple times for the same spot,
/// perhaps with different propagation values. Within the same
/// growth/propagation distance, filler will be called from values
/// propagated from earlier seeds before values propagated from later
/// seeds.
/// </para>
/// <para>
/// filler is not called for positions outside of cellLayer EXCEPT for
/// points being processed as seed values.
/// </para>
/// </summary>
public static void FloodFill<T, P>(
CellLayer<T> cellLayer,
IEnumerable<(CPos CPos, P Prop)> seeds,
Func<CPos, P, P?> filler,
ImmutableArray<CVec> spread) where P : struct
{
var next = seeds.ToList();
while (next.Count != 0)
{
var current = next;
next = new List<(CPos, P)>();
foreach (var (source, prop) in current)
{
var newProp = filler(source, prop);
if (newProp != null)
foreach (var offset in spread)
{
var destination = source + offset;
if (cellLayer.Contains(destination))
next.Add((destination, (P)newProp));
}
}
}
}
}
}

View File

@@ -0,0 +1,296 @@
#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.Immutable;
using System.Linq;
namespace OpenRA.Mods.Common.MapGenerator
{
/// <summary>
/// Utilities for simple directions and adjacency. Note that coordinate systems might not agree
/// as to which directions are conceptually left/right or up/down.
/// </summary>
public static class Direction
{
/// <summary>No direction.</summary>
public const int None = -1;
/// <summary>+X ("right").</summary>
public const int R = 0;
/// <summary>+X+Y ("right down").</summary>
public const int RD = 1;
/// <summary>+Y ("down").</summary>
public const int D = 2;
/// <summary>-X+Y ("left down").</summary>
public const int LD = 3;
/// <summary>-X ("left").</summary>
public const int L = 4;
/// <summary>-X-Y ("left up").</summary>
public const int LU = 5;
/// <summary>-Y ("up").</summary>
public const int U = 6;
/// <summary>+X-Y ("right up").</summary>
public const int RU = 7;
/// <summary>Bitmask right.</summary>
public const int MR = 1 << R;
/// <summary>Bitmask right-down.</summary>
public const int MRD = 1 << RD;
/// <summary>Bitmask down.</summary>
public const int MD = 1 << D;
/// <summary>Bitmask left-down.</summary>
public const int MLD = 1 << LD;
/// <summary>Bitmask left.</summary>
public const int ML = 1 << L;
/// <summary>Bitmask left-up.</summary>
public const int MLU = 1 << LU;
/// <summary>Bitmask up.</summary>
public const int MU = 1 << U;
/// <summary>Bitmask right-up.</summary>
public const int MRU = 1 << RU;
/// <summary>Adjacent offsets with directions, excluding diagonals.</summary>
public static readonly ImmutableArray<(int2, int)> Spread4D = ImmutableArray.Create(new[]
{
(new int2(1, 0), R),
(new int2(0, 1), D),
(new int2(-1, 0), L),
(new int2(0, -1), U)
});
/// <summary>Adjacent offsets, excluding diagonals.</summary>
public static readonly ImmutableArray<int2> Spread4 =
Spread4D.Select(((int2 XY, int _) v) => v.XY).ToImmutableArray();
/// <summary>
/// Adjacent offsets, excluding diagonals. Assumes that CVec(1, 0)
/// corresponds to Direction.R.
/// </summary>
public static readonly ImmutableArray<CVec> Spread4CVec =
Spread4.Select(xy => new CVec(xy.X, xy.Y)).ToImmutableArray();
/// <summary>Adjacent offsets with directions, including diagonals.</summary>
public static readonly ImmutableArray<(int2, int)> Spread8D = ImmutableArray.Create(new[]
{
(new int2(1, 0), R),
(new int2(1, 1), RD),
(new int2(0, 1), D),
(new int2(-1, 1), LD),
(new int2(-1, 0), L),
(new int2(-1, -1), LU),
(new int2(0, -1), U),
(new int2(1, -1), RU)
});
/// <summary>Adjacent offsets, including diagonals.</summary>
public static readonly ImmutableArray<int2> Spread8 =
Spread8D.Select(((int2 XY, int _) v) => v.XY).ToImmutableArray();
/// <summary>
/// Adjacent offsets, including diagonals. Assumes that CVec(1, 0)
/// corresponds to Direction.R.
/// </summary>
public static readonly ImmutableArray<CVec> Spread8CVec =
Spread8.Select(xy => new CVec(xy.X, xy.Y)).ToImmutableArray();
/// <summary>Convert a non-none direction to an int2 offset.</summary>
public static int2 ToInt2(int d)
{
if (d >= 0 && d < 8)
return Spread8[d];
else
throw new ArgumentException("bad direction");
}
/// <summary>
/// Convert a non-none direction to a CVec offset. Assumes that
/// CVec(1, 0) corresponds to Direction.R.
/// </summary>
public static CVec ToCVec(int d)
{
if (d >= 0 && d < 8)
return Spread8CVec[d];
else
throw new ArgumentException("bad direction");
}
/// <summary>
/// Convert an offset (of arbitrary non-zero magnitude) to a direction.
/// Supplying a zero-offset will throw.
/// </summary>
public static int FromOffset(int dx, int dy)
{
if (dx > 0)
{
if (dy > 0)
return RD;
else if (dy < 0)
return RU;
else
return R;
}
else if (dx < 0)
{
if (dy > 0)
return LD;
else if (dy < 0)
return LU;
else
return L;
}
else
{
if (dy > 0)
return D;
else if (dy < 0)
return U;
else
throw new ArgumentException("Bad direction");
}
}
/// <summary>
/// Convert an offset (of arbitrary non-zero magnitude) to a direction.
/// Supplying a zero-offset will throw.
/// </summary>
public static int FromInt2(int2 delta)
=> FromOffset(delta.X, delta.Y);
/// <summary>
/// Convert an offset (of arbitrary non-zero magnitude) to a direction.
/// Supplying a zero-offset will throw. Assumes that CVec(1, 0)
/// corresponds to Direction.R.
/// </summary>
public static int FromCVec(CVec delta)
=> FromOffset(delta.X, delta.Y);
/// <summary>
/// Convert an offset (of arbitrary non-zero magnitude) to a non-diagonal direction.
/// Supplying a zero-offset will throw.
/// </summary>
public static int FromOffsetNonDiagonal(int dx, int dy)
{
if (dx - dy > 0 && dx + dy >= 0)
return R;
if (dy + dx > 0 && dy - dx >= 0)
return D;
if (-dx + dy > 0 && -dx - dy >= 0)
return L;
if (-dy - dx > 0 && -dy + dx >= 0)
return U;
throw new ArgumentException("bad direction");
}
/// <summary>
/// Convert an offset (of arbitrary non-zero magnitude) to a
/// non-diagonal direction. Supplying a zero-offset will throw.
/// </summary>
public static int FromInt2NonDiagonal(int2 delta)
=> FromOffsetNonDiagonal(delta.X, delta.Y);
/// <summary>
/// Convert an offset (of arbitrary non-zero magnitude) to a
/// non-diagonal direction. Supplying a zero-offset will throw. Assumes
/// that CVec(1, 0) corresponds to Direction.R.
/// </summary>
public static int FromCVecNonDiagonal(CVec delta)
=> FromOffsetNonDiagonal(delta.X, delta.Y);
/// <summary>Return the opposite direction.</summary>
public static int Reverse(int direction)
{
if (direction == None)
return None;
return direction ^ 4;
}
/// <summary>Convert a direction to a short string, like "None", "R", "RD", etc.</summary>
public static string ToString(int direction)
{
switch (direction)
{
case None: return "None";
case R: return "R";
case RD: return "RD";
case D: return "D";
case LD: return "LD";
case L: return "L";
case LU: return "LU";
case U: return "U";
case RU: return "RU";
default: throw new ArgumentException("bad direction");
}
}
/// <summary>Count the number of set bits in a direction mask.</summary>
public static int Count(int dm)
{
var count = 0;
for (var m = dm; m != 0; m >>= 1)
if ((m & 1) == 1)
count++;
return count;
}
/// <summary>Finds the only direction set in a direction mask or returns NONE.</summary>
public static int FromMask(int mask)
{
switch (mask)
{
case MR: return R;
case MRD: return RD;
case MD: return D;
case MLD: return LD;
case ML: return L;
case MLU: return LU;
case MU: return U;
case MRU: return RU;
default: return None;
}
}
/// <summary>True if diagonal, false if horizontal/vertical, throws otherwise.</summary>
public static bool IsDiagonal(int direction)
{
switch (direction)
{
case R:
case D:
case L:
case U:
return false;
case RD:
case LD:
case LU:
case RU:
return true;
default:
throw new ArgumentException("NONE or bad direction");
}
}
}
}

View File

@@ -0,0 +1,364 @@
#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;
namespace OpenRA.Mods.Common.MapGenerator
{
public sealed class MapGeneratorSettings
{
/// <summary>How an option should be treated for UI purposes.</summary>
public enum UiType
{
Hidden,
DropDown,
Checkbox,
Integer,
Float,
String,
}
/// <summary>Represents a bunch of settings, along with UI information.</summary>
public sealed class Choice
{
/// <summary>Uniquely identifies a Choice within an Option.</summary>
[FieldLoader.Ignore]
public readonly string Id;
/// <summary>The label to use for UI selection. Post-fluent.</summary>
[FieldLoader.Ignore]
public readonly string Label = null;
/// <summary>Only offer the Choice for this tileset. (If null, show for all.)</summary>
public readonly string Tileset = null;
/// <summary>(Partial) settings to combine into the final overall settings.</summary>
[FieldLoader.LoadUsing(nameof(SettingsLoader))]
public readonly MiniYaml Settings;
public Choice(string id, MiniYaml my)
{
Id = id;
FieldLoader.Load(this, my);
var label = my.NodeWithKeyOrDefault("Label")?.Value.Value;
if (label != null)
Label = FluentProvider.GetMessage(label);
}
/// <summary>Create a choice that represents a top-level setting with a given value.</summary>
public Choice(string setting, string value)
{
Id = value;
Label = value;
Tileset = null;
Settings = new MiniYaml(null, new[] { new MiniYamlNode(setting, value) });
}
public static void DumpFluent(MiniYaml my, List<string> references)
{
var label = my.NodeWithKeyOrDefault("Label")?.Value.Value;
if (label != null)
references.Add(label);
}
static MiniYaml SettingsLoader(MiniYaml my) => my.NodeWithKey("Settings").Value;
/// <summary>Check whether this choice is permitted for this map.</summary>
public bool Allowed(Map map)
{
if (Tileset != null && map.Tileset != Tileset)
return false;
return true;
}
/// <summary>For single-setting choices, creates a new choice for that setting with a given value.</summary>
public Choice NewValue(string value)
{
if (Settings.Nodes.Length != 1)
throw new InvalidOperationException("NewValue can only be used on single-setting Choices");
return new(Settings.Nodes[0].Key, value);
}
}
public sealed class Option
{
/// <summary>Unique identifier for this Option.</summary>
[FieldLoader.Ignore]
public readonly string Id;
/// <summary>The label to use for UI selection. Post-fluent.</summary>
[FieldLoader.Ignore]
public readonly string Label = null;
/// <summary>
/// <para>For multiple-choice options (dropdowns/checkboxes) holds the allowed Choices.</para>
/// <para>For text entry Options, contains the default value.</para>
/// <para>If this is empty, the map is not compatible with the generator.</para>
/// </summary>
[FieldLoader.Ignore]
public readonly IReadOnlyList<Choice> Choices;
/// <summary>
/// <para>The default Choice for this option.</para>
/// <para>Default will be ignored if it is not valid for the map.</para>
/// </summary>
[FieldLoader.Ignore]
public readonly Choice Default;
public readonly double Min = double.NegativeInfinity;
public readonly double Max = double.PositiveInfinity;
/// <summary>Whether the Option can be randomized.</summary>
public readonly bool Random = false;
/// <summary>How an option should be treated for UI purposes.</summary>
[FieldLoader.Ignore]
public readonly UiType Ui;
/// <summary>Settings layering priority. Higher overrides lower.</summary>
public readonly int Priority = 0;
public Option(string id, MiniYaml my, Map map)
{
Id = id;
FieldLoader.Load(this, my);
var label = my.NodeWithKeyOrDefault("Label")?.Value.Value;
if (label != null)
Label = FluentProvider.GetMessage(label);
var choices = new List<Choice>();
Choices = choices;
var integerSetting = my.NodeWithKeyOrDefault("Integer");
var floatSetting = my.NodeWithKeyOrDefault("Float");
var stringSetting = my.NodeWithKeyOrDefault("String");
var checkboxSetting = my.NodeWithKeyOrDefault("Checkbox");
var simpleChoiceSetting = my.NodeWithKeyOrDefault("SimpleChoice");
var textSetting = integerSetting ?? floatSetting ?? stringSetting;
if (textSetting != null)
{
var setting = textSetting.Value.Value;
var value = my.NodeWithKeyOrDefault("Default")?.Value.Value ?? "";
var choice = new Choice(setting, value);
choices.Add(choice);
Default = choice;
Ui =
integerSetting != null ? UiType.Integer :
floatSetting != null ? UiType.Float :
UiType.String;
if (Ui == UiType.Integer)
{
if (Min == double.NegativeInfinity)
Min = int.MinValue;
if (Max == double.PositiveInfinity)
Max = int.MaxValue;
}
}
else if (checkboxSetting != null)
{
var setting = checkboxSetting.Value.Value;
var falseChoice = new Choice(setting, "False");
var trueChoice = new Choice(setting, "True");
choices.Add(falseChoice);
choices.Add(trueChoice);
var enabled = (my.NodeWithKeyOrDefault("Default")?.Value.Value ?? "False") == "True";
Default = enabled ? trueChoice : falseChoice;
Ui = UiType.Checkbox;
}
else
{
if (simpleChoiceSetting != null)
{
var setting = simpleChoiceSetting.Value.Value;
var values = simpleChoiceSetting.Value
.NodeWithKey("Values").Value.Value
.Split(',');
foreach (var value in values)
choices.Add(new Choice(setting, value));
}
else
{
foreach (var node in my.Nodes)
{
var split = node.Key.Split('@');
if (split[0] == "Choice")
{
string choiceId = null;
if (split.Length >= 2)
choiceId = split[1];
var choice = new Choice(choiceId, node.Value);
if (choice.Allowed(map))
choices.Add(choice);
}
}
}
if (Choices.Count > 0)
{
var defaultNode = my.NodeWithKeyOrDefault("Default");
if (defaultNode != null)
{
Default = Choices.FirstOrDefault(choice => choice.Id == defaultNode.Value.Value);
if (Default == null)
throw new YamlException($"Option `{id}` default choice `{defaultNode.Value.Value}` is not valid");
}
else
{
Default = Choices[0];
}
}
Ui = Label == null ? UiType.Hidden : UiType.DropDown;
}
}
public static void DumpFluent(MiniYaml my, List<string> references)
{
var label = my.NodeWithKeyOrDefault("Label")?.Value.Value;
if (label != null)
references.Add(label);
foreach (var node in my.Nodes)
if (node.Key.Split('@')[0] == "Choice")
Choice.DumpFluent(node.Value, references);
}
public bool ValidateChoice(Choice choice)
{
switch (Ui)
{
case UiType.Integer:
{
var valid = long.TryParse(choice.Id, out var value);
if (value < Min || value > Max)
valid = false;
return valid;
}
case UiType.Float:
{
var valid = double.TryParse(choice.Id, out var value);
if (value < Min || value > Max)
valid = false;
return valid;
}
case UiType.String:
return true;
default:
return Choices.Contains(choice);
}
}
public Choice RandomChoice()
{
if (Random)
{
var random = (long)Guid.NewGuid().GetHashCode() - int.MinValue;
switch (Ui)
{
case UiType.Integer:
var intRandom = (int)(random % (long)(Max - Min + 1) + (long)Min);
return Default.NewValue(intRandom.ToStringInvariant());
case UiType.Float:
var floatRandom = (float)((double)0xffffffff * random / (Max - Min) + Min);
return Default.NewValue(floatRandom.ToStringInvariant());
case UiType.Checkbox:
case UiType.DropDown:
if (Choices.Count == 0)
throw new InvalidOperationException($"Map is not compatible with Option `{Id}`");
return Choices[(int)(random % Choices.Count)];
default:
throw new InvalidOperationException($"Option `{Id}` does not have a randomizable type");
}
}
else
{
return Default;
}
}
}
public readonly IReadOnlyList<Option> Options;
/// <summary>
/// Parse settings from a MiniYaml definition. Returns null if the map isn't compatible.
/// </summary>
public static MapGeneratorSettings LoadSettings(MiniYaml my, Map map)
{
var options = new List<Option>();
foreach (var node in my.Nodes)
{
var split = node.Key.Split('@');
if (split[0] == "Option")
{
string id = null;
if (split.Length >= 2)
id = split[1];
var option = new Option(id, node.Value, map);
if (option.Choices.Count == 0)
return null;
options.Add(option);
}
}
return new MapGeneratorSettings(options);
}
public static List<string> DumpFluent(MiniYaml my)
{
var references = new List<string>();
DumpFluent(my, references);
return references;
}
public static void DumpFluent(MiniYaml my, List<string> references)
{
foreach (var node in my.Nodes)
if (node.Key.Split('@')[0] == "Option")
Option.DumpFluent(node.Value, references);
}
MapGeneratorSettings(IReadOnlyList<Option> options)
{
Options = options;
}
public Dictionary<Option, Choice> DefaultChoices()
{
return Options.ToDictionary(option => option, option => option.Default);
}
/// <summary>Merge all choices into a complete settings MiniYaml.</summary>
public MiniYaml Compile(IReadOnlyDictionary<Option, Choice> choices)
{
var layers = new List<IReadOnlyCollection<MiniYamlNode>>();
// Apply the choices in their canonical order.
foreach (var option in Options.OrderBy(option => option.Priority))
{
var choice = choices[option];
if (!option.ValidateChoice(choice))
throw new ArgumentException($"Option `{option.Id}` has illegal choice");
layers.Add(choice.Settings.Nodes);
}
var settingsNodes = MiniYaml.Merge(layers);
return new MiniYaml(null, settingsNodes);
}
}
}

View File

@@ -0,0 +1,186 @@
#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;
namespace OpenRA.Mods.Common.MapGenerator
{
/// <summary>
/// A fixed-size 2D array that can be indexed either linearly or by coordinates.
/// </summary>
public sealed class Matrix<T>
{
/// <summary>Underlying matrix data.</summary>
public readonly T[] Data;
/// <summary>Matrix dimensions.</summary>
public readonly int2 Size;
/// <summary>
/// Create a new matrix with the given size and adopt a given array as its backing data.
/// </summary>
Matrix(int2 size, T[] data)
{
if (data.Length != size.X * size.Y)
throw new ArgumentException("Matrix data length does not match size");
Data = data;
Size = size;
}
/// <summary>Create a new matrix with the given size.</summary>
public Matrix(int2 size)
: this(size, new T[size.X * size.Y])
{ }
/// <summary>Create a new matrix with the given size.</summary>
public Matrix(int x, int y)
: this(new int2(x, y))
{ }
/// <summary>
/// Convert a pair of coordinates into an index into Data.
/// </summary>
public int Index(int2 xy)
=> Index(xy.X, xy.Y);
/// <summary>
/// Convert a pair of coordinates into an index into Data.
/// </summary>
public int Index(int x, int y)
{
if (!ContainsXY(x, y))
throw new IndexOutOfRangeException(
$"({x}, {y}) is out of bounds for a matrix of size ({Size.X}, {Size.Y})");
return y * Size.X + x;
}
/// <summary>
/// Convert a Data index into a pair of coordinates.
/// </summary>
public int2 XY(int index)
{
if (index < 0 || index >= Data.Length)
throw new IndexOutOfRangeException(
$"Index {index} is out of range for a matrix of size ({Size.X}, {Size.Y})");
var y = Math.DivRem(index, Size.X, out var x);
return new int2(x, y);
}
public T this[int x, int y]
{
get => Data[Index(x, y)];
set => Data[Index(x, y)] = value;
}
public T this[int2 xy]
{
get => Data[Index(xy.X, xy.Y)];
set => Data[Index(xy.X, xy.Y)] = value;
}
/// <summary>Shorthand for Data[i].</summary>
public T this[int i]
{
get => Data[i];
set => Data[i] = value;
}
/// <summary>True iff xy is a valid index within the matrix.</summary>
public bool ContainsXY(int2 xy)
{
return xy.X >= 0 && xy.X < Size.X && xy.Y >= 0 && xy.Y < Size.Y;
}
/// <summary>True iff (x, y) is a valid index within the matrix.</summary>
public bool ContainsXY(int x, int y)
{
return x >= 0 && x < Size.X && y >= 0 && y < Size.Y;
}
/// <summary>Clamp xy to be the closest index within the matrix.</summary>
public int2 ClampXY(int2 xy)
{
var (nx, ny) = ClampXY(xy.X, xy.Y);
return new int2(nx, ny);
}
/// <summary>Clamp (x, y) to be the closest index within the matrix.</summary>
public (int Nx, int Ny) ClampXY(int x, int y)
{
if (x >= Size.X)
x = Size.X - 1;
if (x < 0)
x = 0;
if (y >= Size.Y)
y = Size.Y - 1;
if (y < 0)
y = 0;
return (x, y);
}
/// <summary>
/// Creates a transposed (shallow) copy of the matrix.
/// </summary>
public Matrix<T> Transpose()
{
var transposed = new Matrix<T>(new int2(Size.Y, Size.X));
for (var y = 0; y < Size.Y; y++)
for (var x = 0; x < Size.X; x++)
transposed[y, x] = this[x, y];
return transposed;
}
/// <summary>
/// Return a new matrix with the same shape as this one containing the values after being
/// transformed by a mapping func.
/// </summary>
public Matrix<R> Map<R>(Func<T, R> func)
{
var mapped = new Matrix<R>(Size);
for (var i = 0; i < Data.Length; i++)
mapped.Data[i] = func(Data[i]);
return mapped;
}
/// <summary>
/// Replace all values in the matrix with a given value. Returns this.
/// </summary>
public Matrix<T> Fill(T value)
{
Array.Fill(Data, value);
return this;
}
/// <summary>
/// Return a shallow clone of this matrix.
/// </summary>
public Matrix<T> Clone()
{
return new Matrix<T>(Size, (T[])Data.Clone());
}
/// <summary>
/// Combine two same-shape matrices into a new output matrix.
/// The zipping function specifies how values are combined.
/// </summary>
public static Matrix<T> Zip<T1, T2>(Matrix<T1> a, Matrix<T2> b, Func<T1, T2, T> func)
{
if (a.Size != b.Size)
throw new ArgumentException("Input matrices to Zip must match in shape and size");
var matrix = new Matrix<T>(a.Size);
for (var i = 0; i < a.Data.Length; i++)
matrix.Data[i] = func(a.Data[i], b.Data[i]);
return matrix;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,455 @@
#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
{
/// <summary>
/// MiniYaml-loaded definition of a MultiBrush. Can be loaded into a MultiBrush once a map is
/// available.
/// </summary>
public sealed class MultiBrushInfo
{
public readonly float Weight;
public readonly ImmutableArray<string> Actors;
public readonly TerrainTile? BackingTile;
public readonly ImmutableArray<ushort> Templates;
public readonly ImmutableArray<TerrainTile> Tiles;
// Currently doesn't support specifying offsets. Add this capability if/when needed.
public MultiBrushInfo(MiniYaml my)
{
Weight = 1.0f;
var actors = new List<string>();
var templates = new List<ushort>();
var tiles = new List<TerrainTile>();
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<MultiBrushInfo> ParseCollection(MiniYaml my)
{
var brushes = new List<MultiBrushInfo>();
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();
}
}
/// <summary>A super template that can be used to paint both tiles and actors.</summary>
sealed class MultiBrush
{
public enum Replaceability
{
/// <summary>Area cannot be replaced by a tile or obstructing actor.</summary>
None = 0,
/// <summary>Area must be replaced by a different tile, and may optionally be given an actor.</summary>
Tile = 1,
/// <summary>Area must be given an actor, but the underlying tile must not change.</summary>
Actor = 2,
/// <summary>Area can be replaced by a tile and/or actor.</summary>
Any = 3,
}
public float Weight;
readonly List<(CVec, TerrainTile)> tiles;
readonly List<ActorPlan> actorPlans;
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 => 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");
}
/// <summary>
/// Create a new empty MultiBrush with a default weight of 1.0.
/// </summary>
public MultiBrush()
{
Weight = 1.0f;
tiles = new List<(CVec, TerrainTile)>();
actorPlans = new List<ActorPlan>();
shape = Array.Empty<CVec>();
}
MultiBrush(MultiBrush other)
{
Weight = other.Weight;
tiles = new List<(CVec, TerrainTile)>(other.tiles);
actorPlans = new List<ActorPlan>(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);
}
/// <summary>Load a named MultiBrush collection from a map's tileset.</summary>
public static ImmutableArray<MultiBrush> LoadCollection(Map map, string name)
{
var templatedTerrainInfo = map.Rules.TerrainInfo as ITemplatedTerrainInfo;
return templatedTerrainInfo.MultiBrushCollections[name]
.Select(info => new MultiBrush(map, info))
.ToImmutableArray();
}
/// <summary>
/// Clone the brush. Note that this does not deep clone any ActorPlans.
/// </summary>
public MultiBrush Clone()
{
return new MultiBrush(this);
}
void UpdateShape()
{
var xys = new HashSet<CVec>();
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();
}
/// <summary>
/// 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).
/// </summary>
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;
}
/// <summary>
/// Add a single tile, optionally with a given offset. By default, it
/// will be positioned under (0, 0).
/// </summary>
public MultiBrush WithTile(TerrainTile tile, CVec? offset = null)
{
tiles.Add((offset ?? new CVec(0, 0), tile));
UpdateShape();
return this;
}
/// <summary>Add an actor (using the ActorPlan's location as an offset).</summary>
public MultiBrush WithActor(ActorPlan actor)
{
actorPlans.Add(actor);
UpdateShape();
return this;
}
/// <summary>
/// <para>For all spaces occupied by the brush, add the given tile.</para>
/// <para>This is useful for adding a backing tile for actors.</para>
/// </summary>
public MultiBrush WithBackingTile(TerrainTile tile)
{
if (Area == 0)
throw new InvalidOperationException("No area");
foreach (var xy in shape)
tiles.Add((xy, tile));
return this;
}
/// <summary>Update the weight.</summary>
public MultiBrush WithWeight(float weight)
{
if (!(weight > 0.0f))
throw new ArgumentException("Weight was not > 0.0");
Weight = weight;
return this;
}
/// <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>If nothing could be painted, throws ArgumentException.</para>
/// </summary>
public void Paint(Map map, List<ActorPlan> 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<ActorPlan> 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);
}
}
/// <summary>
/// Paint an area defined by replace onto map and actorPlans using availableBrushes.
/// </summary>
public static void PaintArea(
Map map,
List<ActorPlan> actorPlans,
CellLayer<Replaceability> replace,
IReadOnlyList<MultiBrush> availableBrushes,
MersenneTwister random)
{
var brushesByAreaDict = new Dictionary<int, List<MultiBrush>>();
foreach (var brush in availableBrushes)
{
if (!brushesByAreaDict.ContainsKey(brush.Area))
brushesByAreaDict.Add(brush.Area, new List<MultiBrush>());
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<int, List<MultiBrush>>(
1,
availableBrushes.Where(o => o.HasActors && o.Area == 1).ToList()));
var size = map.MapSize;
var replaceMposes = new List<MPos>();
var remaining = new CellLayer<bool>(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<CVec> 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;
}
}
}
}
}

View File

@@ -0,0 +1,177 @@
#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 OpenRA.Support;
namespace OpenRA.Mods.Common.MapGenerator
{
public static class NoiseUtils
{
/// <summary>Amplitude proportional to wavelength.</summary>
public static float PinkAmplitude(float wavelength) => wavelength;
/// <summary>
/// <para>
/// Create noise by combining multiple layers of Perlin noise of halving wavelengths.
/// </para>
/// <para>
/// wavelengthScale defines the largest wavelength as a fraction of the largest dimension of
/// the output.
/// </para>
/// <para>
/// ampFunc specifies the amplitude of each wavelength. PinkAmplitude is often a suitable
/// choice.
/// </para>
/// </summary>
public static Matrix<float> FractalNoise(
MersenneTwister random,
int2 size,
float featureSize,
Func<float, float> ampFunc)
{
var span = Math.Max(size.X, size.Y);
var wavelengths = new float[(int)Math.Log2(span)];
for (var i = 0; i < wavelengths.Length; i++)
wavelengths[i] = featureSize / (1 << i);
var noise = new Matrix<float>(size);
foreach (var wavelength in wavelengths)
{
if (wavelength <= 0.5)
break;
var amps = ampFunc(wavelength);
var subSpan = (int)(span / wavelength) + 2;
var subNoise = PerlinNoise(random, subSpan);
// Offsets should align to grid.
// (The wavelength is divided back out later.)
var offsetX = (int)(random.NextFloat() * wavelength);
var offsetY = (int)(random.NextFloat() * wavelength);
for (var y = 0; y < size.Y; y++)
for (var x = 0; x < size.X; x++)
noise[y * size.X + x] +=
amps * MatrixUtils.Interpolate(
subNoise,
(offsetX + x) / wavelength,
(offsetY + y) / wavelength);
}
return noise;
}
/// <summary>
/// 2D Perlin Noise generator without interpolation, producing a span-by-span sized matrix.
/// </summary>
public static Matrix<float> PerlinNoise(MersenneTwister random, int span)
{
var noise = new Matrix<float>(span, span);
const float D = 0.25f;
for (var y = 0; y <= span; y++)
for (var x = 0; x <= span; x++)
{
var phase = MathF.Tau * random.NextFloatExclusive();
var vx = MathF.Cos(phase);
var vy = MathF.Sin(phase);
if (x > 0 && y > 0)
noise[x - 1, y - 1] += vx * -D + vy * -D;
if (x < span && y > 0)
noise[x, y - 1] += vx * D + vy * -D;
if (x > 0 && y < span)
noise[x - 1, y] += vx * -D + vy * D;
if (x < span && y < span)
noise[x, y] += vx * D + vy * D;
}
return noise;
}
/// <summary>
/// <para>
/// Produce symmetric 2D noise by repeatedly applying some generated Perlin noise under
/// rotation and mirroring.
/// </para>
/// <para>
/// Note that the combination of multiple noise values with varying correlations creates a
/// noise with different properties to simple Perlin noise.
/// </para>
/// </summary>
public static Matrix<float> SymmetricFractalNoise(
MersenneTwister random,
int2 size,
int rotations,
Symmetry.Mirror mirror,
float featureSize,
Func<float, float> ampFunc)
{
if (rotations < 1)
throw new ArgumentException("rotations must be >= 1");
// Need higher resolution due to cropping and rotation artifacts
var templateSpan = Math.Max(size.X, size.Y) * 2 + 2;
var templateSize = new int2(templateSpan, templateSpan);
var templateCenter = new float2(templateSpan - 1, templateSpan - 1) / 2.0f;
var template = FractalNoise(random, templateSize, featureSize, ampFunc);
var output = new Matrix<float>(size);
var inclusiveOutputSize = size - new int2(1, 1);
var outputMid = new float2(inclusiveOutputSize) / 2.0f;
for (var y = 0; y < size.Y; y++)
for (var x = 0; x < size.X; x++)
{
const float Sqrt2 = 1.4142135623730951f;
var outputXy = new float2(x, y);
var outputXyFromCenter = outputXy - outputMid;
var templateXyFromCenter = outputXyFromCenter * Sqrt2;
var templateXy = templateXyFromCenter + templateCenter;
var projections = Symmetry.RotateAndMirrorPointAround(
templateXy, templateCenter, rotations, mirror);
foreach (var projection in projections)
output[x, y] +=
MatrixUtils.Interpolate(
template,
projection.X,
projection.Y);
}
return output;
}
/// <summary>
/// Use SymmetricFractalNoise to fill a CellLayer. The noise is aligned to the CPos
/// coordinate system.
/// </summary>
public static void SymmetricFractalNoiseIntoCellLayer(
MersenneTwister random,
CellLayer<float> cellLayer,
int rotations,
Symmetry.Mirror mirror,
float featureSize,
Func<float, float> ampFunc)
{
var cellBounds = CellLayerUtils.CellBounds(cellLayer);
var size = new int2(cellBounds.Size.Width, cellBounds.Size.Height);
var noise = SymmetricFractalNoise(
random,
size,
rotations,
mirror,
featureSize,
ampFunc);
CellLayerUtils.FromMatrix(cellLayer, noise);
}
}
}

View File

@@ -0,0 +1,148 @@
#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.Collections.Generic;
namespace OpenRA.Mods.Common.MapGenerator
{
public static class PlayableSpace
{
public enum Playability
{
/// <summary>Area is unplayable by land/naval units.</summary>
Unplayable = 0,
/// <summary>
/// Area is unplayable by land/naval units, but should count as
/// being "within" a playable region. This usually applies to random
/// rock or river tiles in largely passable templates.
/// </summary>
Partial = 1,
/// <summary>
/// Area is playable by either land or naval units.
/// </summary>
Playable = 2,
}
/// <summary>
/// Additional data for a region containing playable space.
/// The shape of a region is specified separately via a region mask.
/// </summary>
public sealed class Region
{
/// <summary>Area of playable and partially playable space.</summary>
public int Area;
/// <summary>Area of fully playable space.</summary>
public int PlayableArea;
/// <summary>Region ID.</summary>
public int Id;
}
/// <summary>Sentinel indicating a position isn't assigned to a region.</summary>
public const int NullRegion = -1;
/// <summary>
/// <para>
/// Analyses a given map's tiles and ActorPlans and determines the playable space within it.
/// </para>
/// <para>
/// Requires a playabilityMap which specifies whether certain tiles are considered playable
/// or not. Actors are always considered partially playable.
/// </para>
/// <para>
/// RegionMap contains the mapping of map positions to Regions. If a map position is not
/// within a region, the value is NullRegion.
/// </para>
/// </summary>
public static (Region[] Regions, CellLayer<int> RegionMap, CellLayer<Playability> Playable) FindPlayableRegions(
Map map,
List<ActorPlan> actorPlans,
Dictionary<TerrainTile, Playability> playabilityMap)
{
var size = map.MapSize;
var regions = new List<Region>();
var regionMap = new CellLayer<int>(map);
regionMap.Clear(NullRegion);
var playable = new CellLayer<Playability>(map);
playable.Clear(Playability.Unplayable);
foreach (var mpos in map.AllCells.MapCoords)
if (map.Contains(mpos))
playable[mpos] = playabilityMap[map.Tiles[mpos]];
foreach (var actorPlan in actorPlans)
foreach (var cpos in actorPlan.Footprint().Keys)
if (map.AllCells.Contains(cpos))
{
var mpos = cpos.ToMPos(map);
if (playable[mpos] == Playability.Playable)
playable[mpos] = Playability.Partial;
}
void Fill(Region region, CPos start)
{
void AddToRegion(CPos cpos, bool fullyPlayable)
{
var mpos = cpos.ToMPos(map);
regionMap[mpos] = region.Id;
region.Area++;
if (fullyPlayable)
region.PlayableArea++;
}
bool? Filler(CPos cpos, bool fullyPlayable)
{
var mpos = cpos.ToMPos(map);
if (regionMap[mpos] == NullRegion)
{
if (fullyPlayable && playable[mpos] == Playability.Playable)
{
AddToRegion(cpos, true);
return true;
}
else if (playable[mpos] == Playability.Partial)
{
AddToRegion(cpos, false);
return false;
}
}
return null;
}
CellLayerUtils.FloodFill(
playable,
new[] { (start, true) },
Filler,
Direction.Spread4CVec);
}
foreach (var mpos in map.AllCells.MapCoords)
if (regionMap[mpos] == NullRegion && playable[mpos] == Playability.Playable)
{
var region = new Region()
{
Area = 0,
PlayableArea = 0,
Id = regions.Count,
};
regions.Add(region);
var cpos = mpos.ToCPos(map);
Fill(region, cpos);
}
return (regions.ToArray(), regionMap, playable);
}
}
}

View File

@@ -0,0 +1,319 @@
#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;
namespace OpenRA.Mods.Common.MapGenerator
{
public static class Symmetry
{
/// <summary>Trivial mirroring configurations defined in world space.</summary>
public enum Mirror
{
/// <summary>No mirror.</summary>
None = 0,
/// <summary>Match low X with high X in WPos space.</summary>
LeftMatchesRight = 1,
/// <summary>Match low X, low Y with high X, high Y in WPos space.</summary>
TopLeftMatchesBottomRight = 2,
/// <summary>Match low Y with high Y in WPos space.</summary>
TopMatchesBottom = 3,
/// <summary>Match low X, high Y with high X, low Y in WPos space.</summary>
TopRightMatchesBottomLeft = 4,
}
public static bool TryParseMirror(string s, out Mirror mirror) => Enum.TryParse(s, out mirror);
/// <summary>
/// <para>
/// Mirrors a (zero-area) point around a given center.
/// </para>
/// <para>
/// For example, if using a center of (4.0, 4.0) a point at (0.1, 0.1) could be projected
/// to (0.1, 0.1), (0.1, 7.9), (7.9, 0.1), or (7.9, 7.9).
/// </para>
/// </summary>
public static float2 MirrorPointAround(Mirror mirror, float2 original, float2 center)
{
switch (mirror)
{
case Mirror.None:
throw new ArgumentException("Mirror.None has no transformed point");
case Mirror.LeftMatchesRight:
return new float2(2.0f * center.X - original.X, original.Y);
case Mirror.TopLeftMatchesBottomRight:
return new float2(
center.Y - original.Y + center.X,
center.X - original.X + center.Y);
case Mirror.TopMatchesBottom:
return new float2(original.X, 2.0f * center.Y - original.Y);
case Mirror.TopRightMatchesBottomLeft:
return new float2(
center.X + original.Y - center.Y,
center.Y + original.X - center.X);
default:
throw new ArgumentException("Bad mirror");
}
}
public static WPos MirrorWPosAround(Mirror mirror, WPos original, WPos center)
{
switch (mirror)
{
case Mirror.None:
throw new ArgumentException("Mirror.None has no transformed point");
case Mirror.LeftMatchesRight:
return new WPos(2 * center.X - original.X, original.Y, original.Z);
case Mirror.TopLeftMatchesBottomRight:
return new WPos(
center.Y - original.Y + center.X,
center.X - original.X + center.Y,
original.Z);
case Mirror.TopMatchesBottom:
return new WPos(original.X, 2 * center.Y - original.Y, original.Z);
case Mirror.TopRightMatchesBottomLeft:
return new WPos(
center.X + original.Y - center.Y,
center.Y + original.X - center.X,
original.Z);
default:
throw new ArgumentException("Bad mirror");
}
}
/// <summary>
/// Given rotation and mirror parameters, return the total number of projected points this
/// would result in (including the original point).
/// </summary>
public static int RotateAndMirrorProjectionCount(int rotations, Mirror mirror)
=> mirror == Mirror.None ? rotations : rotations * 2;
public static WPos[] RotateAndMirrorWPosAround(
WPos original,
WPos center,
int rotations,
Mirror mirror)
{
var projections = new WPos[RotateAndMirrorProjectionCount(rotations, mirror)];
var projectionIndex = 0;
for (var rotation = 0; rotation < rotations; rotation++)
{
// This could be made more accurate using dedicated, higher precision
// rotation count to cos and sin lookup tables.
var wangle = new WAngle(rotation * 1024 / rotations);
var cos1024 = wangle.Cos();
var sin1024 = wangle.Sin();
var relOrig = original - center;
var projX = (relOrig.X * cos1024 - relOrig.Y * sin1024) / 1024 + center.X;
var projY = (relOrig.X * sin1024 + relOrig.Y * cos1024) / 1024 + center.Y;
var projection = new WPos(projX, projY, original.Z);
projections[projectionIndex++] = projection;
if (mirror != Mirror.None)
projections[projectionIndex++] = MirrorWPosAround(mirror, projection, center);
}
return projections;
}
public static WPos[] RotateAndMirrorWPos<T>(
WPos original,
CellLayer<T> cellLayer,
int rotations,
Mirror mirror)
{
return RotateAndMirrorWPosAround(
original,
CellLayerUtils.Center(cellLayer),
rotations,
mirror);
}
public static CPos[] RotateAndMirrorCPos<T>(
CPos original,
CellLayer<T> cellLayer,
int rotations,
Mirror mirror)
{
var cposProjections = new CPos[RotateAndMirrorProjectionCount(rotations, mirror)];
var wpos = CellLayerUtils.CPosToWPos(original, cellLayer.GridType);
var wposProjections = RotateAndMirrorWPos(wpos, cellLayer, rotations, mirror);
for (var i = 0; i < wposProjections.Length; i++)
cposProjections[i] = CellLayerUtils.WPosToCPos(wposProjections[i], cellLayer.GridType);
return cposProjections;
}
/// <summary>
/// Determine the shortest distance between projected positions.
/// </summary>
public static int ProjectionProximity(int2[] projections)
{
if (projections.Length == 1)
return int.MaxValue;
var worstSpacingSq = long.MaxValue;
for (var i1 = 0; i1 < projections.Length; i1++)
for (var i2 = 0; i2 < projections.Length; i2++)
{
if (i1 == i2)
continue;
var spacingSq = (projections[i1] - projections[i2]).LengthSquared;
if (spacingSq < worstSpacingSq)
worstSpacingSq = spacingSq;
}
return (int)Math.Sqrt(worstSpacingSq);
}
/// <summary>
/// Determine the shortest distance between projected positions.
/// </summary>
public static int ProjectionProximity(CPos[] projections)
{
return ProjectionProximity(
projections.Select(cpos => new int2(cpos.X, cpos.Y)).ToArray());
}
/// <summary>
/// <para>
/// Duplicate an original point into an array of projected points according to a rotation
/// and mirror specification.
/// </para>
/// <para>
/// Rotations use WAngel-based trigonometric math for consistency with other Symmetry
/// functions. This may be slightly imprecise for non-trivial rotations.
/// </para>
/// <para>
/// For example, if using a center of (4.0, 4.0) a point at (0.1, 0.1) could be projected
/// to (0.1, 0.1), (0.1, 7.9), (7.9, 0.1), and (7.9, 7.9).
/// </para>
/// </summary>
public static float2[] RotateAndMirrorPointAround(
float2 original,
float2 center,
int rotations,
Mirror mirror)
{
var projections = new float2[RotateAndMirrorProjectionCount(rotations, mirror)];
var projectionIndex = 0;
for (var rotation = 0; rotation < rotations; rotation++)
{
// This could be made more accurate using dedicated, higher precision
// rotation count to cos and sin lookup tables.
var wangle = new WAngle(rotation * 1024 / rotations);
var cos = wangle.Cos() / 1024.0f;
var sin = wangle.Sin() / 1024.0f;
var relOrig = original - center;
var projX = relOrig.X * cos - relOrig.Y * sin + center.X;
var projY = relOrig.X * sin + relOrig.Y * cos + center.Y;
var projection = new float2(projX, projY);
projections[projectionIndex++] = projection;
if (mirror != Mirror.None)
projections[projectionIndex++] = MirrorPointAround(mirror, projection, center);
}
return projections;
}
/// <summary>
/// Rotate and mirror multiple actor plans. See RotateAndMirrorActorPlan.
/// </summary>
public static ImmutableArray<ActorPlan> RotateAndMirrorActorPlans(
IReadOnlyList<ActorPlan> originals,
int rotations,
Mirror mirror)
{
var projections = new List<ActorPlan>(
originals.Count * RotateAndMirrorProjectionCount(rotations, mirror));
foreach (var original in originals)
projections.AddRange(RotateAndMirrorActorPlan(original, rotations, mirror));
return projections.ToImmutableArray();
}
/// <summary>
/// Rotate and mirror a single actor plan, adding to an accumulator list.
/// Locations (CPos) are necessarily snapped to grid.
/// </summary>
public static ImmutableArray<ActorPlan> RotateAndMirrorActorPlan(
ActorPlan original,
int rotations,
Mirror mirror)
{
var projections = new List<ActorPlan>(RotateAndMirrorProjectionCount(rotations, mirror));
var points = RotateAndMirrorWPos(
original.WPosCenterLocation,
original.Map.Tiles,
rotations,
mirror);
foreach (var point in points)
{
var plan = original.Clone();
plan.WPosCenterLocation = point;
projections.Add(plan);
}
return projections.ToImmutableArray();
}
/// <summary>
/// Calls action(projections, original) over all possible original
/// CPos positions, where each projection in projections is a
/// mirrored/rotated point. For non-trivial symmetries, projections may
/// be outside the bounds defined by cellLayer.
/// </summary>
public static void RotateAndMirrorOverCPos<T>(
CellLayer<T> cellLayer,
int rotations,
Mirror mirror,
Action<CPos[], CPos> action)
{
var size = cellLayer.Size;
for (var v = 0; v < size.Height; v++)
for (var u = 0; u < size.Width; u++)
{
var original = new MPos(u, v).ToCPos(cellLayer.GridType);
var projections = RotateAndMirrorCPos(original, cellLayer, rotations, mirror);
action(projections, original);
}
}
/// <summary>
/// Returns true iff xy is within reservationRadius of the center of a given CellLayer. If
/// a mirroring is specified, the radius is measured from the mirror line instead of the
/// center point.
/// </summary>
public static bool IsCPosNearCenter<T>(
CPos cpos,
CellLayer<T> cellLayer,
float reservationRadius,
Mirror mirror)
{
CPos[] testPoints;
if (mirror == Mirror.None)
testPoints = RotateAndMirrorCPos(cpos, cellLayer, 2, Mirror.None);
else
testPoints = RotateAndMirrorCPos(cpos, cellLayer, 1, mirror);
var separation = (testPoints[1] - testPoints[0]).LengthSquared;
return separation <= reservationRadius * reservationRadius * 4.0f;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,9 +10,11 @@
#endregion
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Mods.Common.MapGenerator;
using OpenRA.Primitives;
using OpenRA.Support;
@@ -79,6 +81,10 @@ namespace OpenRA.Mods.Common.Terrain
[FieldLoader.Ignore]
public readonly IReadOnlyDictionary<ushort, TerrainTemplateInfo> Templates;
[FieldLoader.Ignore]
public readonly IReadOnlyDictionary<TemplateSegment, TerrainTemplateInfo> SegmentsToTemplates;
[FieldLoader.Ignore]
public readonly IReadOnlyDictionary<string, IEnumerable<MultiBrushInfo>> MultiBrushCollections;
[FieldLoader.Ignore]
public readonly TerrainTypeInfo[] TerrainInfo;
@@ -117,6 +123,20 @@ namespace OpenRA.Mods.Common.Terrain
// Templates
Templates = yaml["Templates"].ToDictionary().Values
.Select(y => (TerrainTemplateInfo)new DefaultTerrainTemplateInfo(this, y)).ToDictionary(t => t.Id);
SegmentsToTemplates = ImmutableDictionary.CreateRange(
Templates.Values.SelectMany(
template => template.Segments.Select(
segment => new KeyValuePair<TemplateSegment, TerrainTemplateInfo>(segment, template))));
MultiBrushCollections =
yaml.TryGetValue("MultiBrushCollections", out var collectionDefinitions)
? collectionDefinitions.ToDictionary()
.Select(kv => new KeyValuePair<string, IEnumerable<MultiBrushInfo>>(
kv.Key,
MultiBrushInfo.ParseCollection(kv.Value)))
.ToImmutableDictionary()
: ImmutableDictionary<string, IEnumerable<MultiBrushInfo>>.Empty;
}
public TerrainTypeInfo this[byte index] => TerrainInfo[index];
@@ -167,6 +187,8 @@ namespace OpenRA.Mods.Common.Terrain
string[] ITemplatedTerrainInfo.EditorTemplateOrder => EditorTemplateOrder;
IReadOnlyDictionary<ushort, TerrainTemplateInfo> ITemplatedTerrainInfo.Templates => Templates;
IReadOnlyDictionary<TemplateSegment, TerrainTemplateInfo> ITemplatedTerrainInfo.SegmentsToTemplates => SegmentsToTemplates;
IReadOnlyDictionary<string, IEnumerable<MultiBrushInfo>> ITemplatedTerrainInfo.MultiBrushCollections => MultiBrushCollections;
void ITerrainInfoNotifyMapCreated.MapCreated(Map map)
{

View File

@@ -0,0 +1,63 @@
#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.Text.RegularExpressions;
namespace OpenRA.Mods.Common.Terrain
{
/// <summary>
/// Information about how certain templates (like cliffs, beaches, roads) link together.
/// </summary>
public class TemplateSegment
{
public readonly string Start;
public readonly string Inner;
public readonly string End;
/// <summary>
/// Point sequence, where points are -X-Y corners of template tiles.
/// </summary>
[FieldLoader.Ignore]
public readonly CVec[] Points;
public TemplateSegment(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");
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]));
}
}
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)
=> MatchesType(Inner, matcher);
public bool HasEndType(string matcher)
=> MatchesType(End, matcher);
}
}

View File

@@ -9,7 +9,9 @@
*/
#endregion
using System;
using System.Collections.Generic;
using OpenRA.Mods.Common.MapGenerator;
namespace OpenRA.Mods.Common.Terrain
{
@@ -17,6 +19,8 @@ namespace OpenRA.Mods.Common.Terrain
{
string[] EditorTemplateOrder { get; }
IReadOnlyDictionary<ushort, TerrainTemplateInfo> Templates { get; }
IReadOnlyDictionary<TemplateSegment, TerrainTemplateInfo> SegmentsToTemplates { get; }
IReadOnlyDictionary<string, IEnumerable<MultiBrushInfo>> MultiBrushCollections { get; }
}
public interface ITerrainInfoNotifyMapCreated : ITerrainInfo
@@ -33,6 +37,8 @@ namespace OpenRA.Mods.Common.Terrain
readonly TerrainTileInfo[] tileInfo;
public readonly TemplateSegment[] Segments;
public TerrainTemplateInfo(ITerrainInfo terrainInfo, MiniYaml my)
{
FieldLoader.Load(this, my);
@@ -72,6 +78,19 @@ namespace OpenRA.Mods.Common.Terrain
tileInfo[key] = LoadTileInfo(terrainInfo, node.Value);
}
}
var segmentsNode = my.NodeWithKeyOrDefault("Segments");
if (segmentsNode != null)
{
Segments = new TemplateSegment[segmentsNode.Value.Nodes.Length];
var i = 0;
foreach (var segmentNode in segmentsNode.Value.Nodes)
Segments[i++] = new TemplateSegment(segmentNode.Value);
}
else
{
Segments = Array.Empty<TemplateSegment>();
}
}
protected virtual TerrainTileInfo LoadTileInfo(ITerrainInfo terrainInfo, MiniYaml my)

View File

@@ -0,0 +1,115 @@
#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 OpenRA.Mods.Common.MapGenerator;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Support;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.World | SystemActors.EditorWorld)]
[Desc("A map generator that clears a map.")]
public sealed class ClearMapGeneratorInfo : TraitInfo, IMapGeneratorInfo
{
[FieldLoader.Require]
[Desc("Human-readable name this generator uses.")]
[FluentReference]
public readonly string Name = null;
// This is purely of interest to the linter.
[FieldLoader.LoadUsing(nameof(FluentReferencesLoader))]
[FluentReference]
public readonly List<string> FluentReferences = null;
[FieldLoader.Require]
[Desc("Internal id for this map generator.")]
public readonly string Type = null;
[FieldLoader.LoadUsing(nameof(SettingsLoader))]
public readonly MiniYaml Settings;
string IMapGeneratorInfo.Type => Type;
string IMapGeneratorInfo.Name => Name;
public override object Create(ActorInitializer init) { return new ClearMapGenerator(this); }
static MiniYaml SettingsLoader(MiniYaml my)
{
return my.NodeWithKey("Settings").Value;
}
static List<string> FluentReferencesLoader(MiniYaml my)
{
return MapGeneratorSettings.DumpFluent(my.NodeWithKey("Settings").Value);
}
}
public sealed class ClearMapGenerator : IMapGenerator
{
readonly ClearMapGeneratorInfo info;
IMapGeneratorInfo IMapGenerator.Info => info;
public ClearMapGenerator(ClearMapGeneratorInfo info)
{
this.info = info;
}
public MapGeneratorSettings GetSettings(Map map)
{
return MapGeneratorSettings.LoadSettings(info.Settings, map);
}
public void Generate(Map map, MiniYaml settings)
{
var random = new MersenneTwister();
var tileset = map.Rules.TerrainInfo;
if (!Exts.TryParseUshortInvariant(settings.NodeWithKey("Tile").Value.Value, out var tileType))
throw new YamlException("Illegal tile type");
var tile = new TerrainTile(tileType, 0);
if (!tileset.TryGetTerrainInfo(tile, out var _))
throw new MapGenerationException("Illegal tile type");
// If the default terrain tile is part of a PickAny template, pick
// a random tile index. Otherwise, just use the default tile.
Func<TerrainTile> tilePicker;
if (map.Rules.TerrainInfo is ITemplatedTerrainInfo templatedTerrainInfo &&
templatedTerrainInfo.Templates.TryGetValue(tileType, out var template) &&
template.PickAny)
{
tilePicker = () => new TerrainTile(tileType, (byte)random.Next(0, template.TilesCount));
}
else
{
tilePicker = () => tile;
}
foreach (var cell in map.AllCells)
{
var mpos = cell.ToMPos(map);
map.Tiles[mpos] = tilePicker();
map.Resources[mpos] = new ResourceTile(0, 0);
map.Height[mpos] = 0;
}
map.PlayerDefinitions = new MapPlayers(map.Rules, 0).ToMiniYaml();
map.ActorDefinitions = ImmutableArray<MiniYamlNode>.Empty;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@ using OpenRA.Activities;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Activities;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Mods.Common.MapGenerator;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Mods.Common.Widgets;
using OpenRA.Primitives;
@@ -959,4 +960,40 @@ namespace OpenRA.Mods.Common.Traits
/// </remarks>
bool PathMightExistForLocomotorBlockedByImmovable(Locomotor locomotor, CPos source, CPos target);
}
public class MapGenerationException : Exception
{
public MapGenerationException(string message)
: base(message) { }
public MapGenerationException(string message, Exception inner)
: base(message, inner) { }
}
public interface IMapGeneratorInfo : ITraitInfoInterface
{
string Type { get; }
string Name { get; }
}
public interface IMapGenerator
{
/// <summary>
/// Get the generator settings available for this map.
/// Returns null if not compatible with the given map.
/// </summary>
MapGeneratorSettings GetSettings(Map map);
/// <summary>
/// Generate or manipulate a supplied map in-place.
/// </summary>
/// <exception cref="YamlException">
/// May be thrown if the map settings are invalid. Map should be discarded.
/// </exception>
/// <exception cref="MapGenerationException">
/// Thrown if the map could not be generated with the requested configuration. Map should be discarded.
/// </exception>
void Generate(Map map, MiniYaml settings);
IMapGeneratorInfo Info { get; }
}
}

View File

@@ -0,0 +1,355 @@
#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.Diagnostics;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.EditorBrushes;
using OpenRA.Mods.Common.MapGenerator;
using OpenRA.Mods.Common.Traits;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets.Logic
{
public class MapGeneratorToolLogic : ChromeLogic
{
[FluentReference("name")]
const string StrGenerated = "notification-map-generator-generated";
[FluentReference]
const string StrBadOption = "notification-map-generator-bad-option";
[FluentReference]
const string StrFailed = "notification-map-generator-failed";
[FluentReference]
const string StrFailedCancel = "label-map-generator-failed-cancel";
readonly EditorActionManager editorActionManager;
readonly ButtonWidget generateButtonWidget;
readonly ButtonWidget generateRandomButtonWidget;
readonly World world;
readonly WorldRenderer worldRenderer;
readonly ModData modData;
// nullable
IMapGenerator selectedGenerator;
readonly Dictionary<IMapGenerator, MapGeneratorSettings> generatorsToSettings;
readonly Dictionary<IMapGenerator, Dictionary<MapGeneratorSettings.Option, MapGeneratorSettings.Choice>> generatorsToSettingsChoices;
readonly ScrollPanelWidget settingsPanel;
readonly Widget checkboxSettingTemplate;
readonly Widget textSettingTemplate;
readonly Widget dropDownSettingTemplate;
[ObjectCreator.UseCtor]
public MapGeneratorToolLogic(Widget widget, World world, WorldRenderer worldRenderer, ModData modData)
{
editorActionManager = world.WorldActor.Trait<EditorActionManager>();
this.world = world;
this.worldRenderer = worldRenderer;
this.modData = modData;
selectedGenerator = null;
generatorsToSettings = new Dictionary<IMapGenerator, MapGeneratorSettings>();
generatorsToSettingsChoices = new Dictionary<IMapGenerator, Dictionary<MapGeneratorSettings.Option, MapGeneratorSettings.Choice>>();
var mapGenerators = new List<IMapGenerator>();
foreach (var generator in world.WorldActor.TraitsImplementing<IMapGenerator>())
{
var settings = generator.GetSettings(world.Map);
if (settings == null)
continue;
var choices = settings.DefaultChoices();
mapGenerators.Add(generator);
generatorsToSettingsChoices.Add(generator, choices);
generatorsToSettings.Add(generator, settings);
}
generateButtonWidget = widget.Get<ButtonWidget>("GENERATE_BUTTON");
generateRandomButtonWidget = widget.Get<ButtonWidget>("GENERATE_RANDOM_BUTTON");
settingsPanel = widget.Get<ScrollPanelWidget>("SETTINGS_PANEL");
checkboxSettingTemplate = settingsPanel.Get<Widget>("CHECKBOX_TEMPLATE");
textSettingTemplate = settingsPanel.Get<Widget>("TEXT_TEMPLATE");
dropDownSettingTemplate = settingsPanel.Get<Widget>("DROPDOWN_TEMPLATE");
generateButtonWidget.OnClick = GenerateMap;
generateRandomButtonWidget.OnClick = () =>
{
Randomize();
GenerateMap();
};
var generatorDropDown = widget.Get<DropDownButtonWidget>("GENERATOR");
ChangeGenerator(mapGenerators.FirstOrDefault((IMapGenerator)null));
if (selectedGenerator != null)
{
generatorDropDown.GetText = () => FluentProvider.GetMessage(selectedGenerator.Info.Name);
generatorDropDown.OnMouseDown = _ =>
{
ScrollItemWidget SetupItem(IMapGenerator g, ScrollItemWidget template)
{
bool IsSelected() => g.Info.Type == selectedGenerator.Info.Type;
void OnClick() => ChangeGenerator(mapGenerators.First(generator => generator.Info.Type == g.Info.Type));
var item = ScrollItemWidget.Setup(template, IsSelected, OnClick);
item.Get<LabelWidget>("LABEL").GetText = () => FluentProvider.GetMessage(g.Info.Name);
return item;
}
generatorDropDown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", mapGenerators.Count * 30, mapGenerators, SetupItem);
};
}
else
{
generateButtonWidget.IsDisabled = () => true;
generateRandomButtonWidget.IsDisabled = () => true;
generatorDropDown.IsDisabled = () => true;
}
}
sealed class RandomMapEditorAction : IEditorAction
{
public string Text { get; }
readonly EditorBlit editorBlit;
public RandomMapEditorAction(EditorBlit editorBlit, string description)
{
this.editorBlit = editorBlit;
Text = description;
}
public void Execute()
{
Do();
}
public void Do()
{
editorBlit.Commit();
}
public void Undo()
{
editorBlit.Revert();
}
}
// newGenerator may be null.
void ChangeGenerator(IMapGenerator newGenerator)
{
selectedGenerator = newGenerator;
UpdateSettingsUi();
}
void UpdateSettingsUi()
{
settingsPanel.RemoveChildren();
settingsPanel.ContentHeight = 0;
if (selectedGenerator == null)
return;
var settings = generatorsToSettings[selectedGenerator];
var choices = generatorsToSettingsChoices[selectedGenerator];
foreach (var option in settings.Options)
{
Widget settingWidget;
switch (option.Ui)
{
case MapGeneratorSettings.UiType.Hidden:
continue;
case MapGeneratorSettings.UiType.DropDown:
{
settingWidget = dropDownSettingTemplate.Clone();
var label = settingWidget.Get<LabelWidget>("LABEL");
label.GetText = () => option.Label;
var dropDown = settingWidget.Get<DropDownButtonWidget>("DROPDOWN");
dropDown.GetText = () => choices[option].Label;
dropDown.OnMouseDown = _ =>
{
ScrollItemWidget SetupItem(MapGeneratorSettings.Choice choice, ScrollItemWidget template)
{
bool IsSelected() => choice == choices[option];
void OnClick() => choices[option] = choice;
var item = ScrollItemWidget.Setup(template, IsSelected, OnClick);
item.Get<LabelWidget>("LABEL").GetText = () => choice.Label;
return item;
}
dropDown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", option.Choices.Count * 30, option.Choices, SetupItem);
};
break;
}
case MapGeneratorSettings.UiType.Checkbox:
{
if (option.Choices.Count != 2)
throw new InvalidOperationException("Checkbox option that does not have two choices");
settingWidget = checkboxSettingTemplate.Clone();
var checkbox = settingWidget.Get<CheckboxWidget>("CHECKBOX");
checkbox.GetText = () => option.Label;
checkbox.IsChecked = () => choices[option] == option.Choices[1];
checkbox.OnClick = () =>
choices[option] =
choices[option] == option.Choices[1]
? option.Choices[0]
: option.Choices[1];
break;
}
case MapGeneratorSettings.UiType.Integer:
case MapGeneratorSettings.UiType.Float:
case MapGeneratorSettings.UiType.String:
{
if (option.Choices.Count != 1)
throw new InvalidOperationException("Text option that does not have one choice");
settingWidget = textSettingTemplate.Clone();
var label = settingWidget.Get<LabelWidget>("LABEL");
label.GetText = () => option.Label;
var input = settingWidget.Get<TextFieldWidget>("INPUT");
input.Text = choices[option].Id;
input.OnTextEdited = () =>
{
var choice = choices[option].NewValue(input.Text);
choices[option] = choice;
var valid = option.ValidateChoice(choice);
input.IsValid = () => valid;
};
break;
}
default:
throw new NotSupportedException("Unsupported MapGeneratorSettings.UiType");
}
settingWidget.IsVisible = () => true;
settingsPanel.AddChild(settingWidget);
}
}
void DisplayError(Exception e)
{
// For any non-MapGenerationException, include more information for debugging purposes.
var message = e is MapGenerationException ? e.Message : e.ToString();
Log.Write("debug", e);
ConfirmationDialogs.ButtonPrompt(modData,
title: StrFailed,
text: message,
onCancel: () => { },
cancelText: StrFailedCancel);
}
void Randomize()
{
var choices = generatorsToSettingsChoices[selectedGenerator];
foreach (var option in choices.Keys)
{
if (option.Random)
choices[option] = option.RandomChoice();
}
UpdateSettingsUi();
}
void GenerateMap()
{
try
{
GenerateMapMayThrow();
}
catch (Exception e) when (e is MapGenerationException || e is YamlException)
{
DisplayError(e);
}
}
void GenerateMapMayThrow()
{
var map = world.Map;
var tileset = modData.DefaultTerrainInfo[map.Tileset];
var generatedMap = new Map(modData, tileset, map.MapSize.X, map.MapSize.Y);
var bounds = map.Bounds;
generatedMap.SetBounds(new PPos(bounds.Left, bounds.Top), new PPos(bounds.Right - 1, bounds.Bottom - 1));
var choices = generatorsToSettingsChoices[selectedGenerator];
foreach (var optionChoice in choices)
{
var option = optionChoice.Key;
var choice = optionChoice.Value;
if (!option.ValidateChoice(choice))
throw new MapGenerationException(
FluentProvider.GetMessage(StrBadOption, "option", option.Label));
}
var settings = generatorsToSettings[selectedGenerator].Compile(choices);
// Run main generator logic. May throw.
var generateStopwatch = Stopwatch.StartNew();
Log.Write("debug", $"Running '{selectedGenerator.Info.Type}' map generator with settings:\n{MiniYamlExts.WriteToString(settings.Nodes)}\n\n");
selectedGenerator.Generate(generatedMap, settings);
Log.Write("debug", $"Generator finished, taking {generateStopwatch.ElapsedMilliseconds}ms");
var editorActorLayer = world.WorldActor.Trait<EditorActorLayer>();
var resourceLayer = world.WorldActor.TraitOrDefault<IResourceLayer>();
// Hack, hack, hack.
var resourceTypesByIndex = (resourceLayer.Info as EditorResourceLayerInfo).ResourceTypes.ToDictionary(
kv => kv.Value.ResourceIndex,
kv => kv.Key);
var tiles = new Dictionary<CPos, BlitTile>();
foreach (var cell in generatedMap.AllCells)
{
var mpos = cell.ToMPos(map);
var resourceTile = generatedMap.Resources[mpos];
resourceTypesByIndex.TryGetValue(resourceTile.Type, out var resourceType);
var resourceLayerContents = new ResourceLayerContents(resourceType, resourceTile.Index);
tiles.Add(cell, new BlitTile(generatedMap.Tiles[mpos], resourceTile, resourceLayerContents, generatedMap.Height[mpos]));
}
var previews = new Dictionary<string, EditorActorPreview>();
var players = generatedMap.PlayerDefinitions.Select(pr => new PlayerReference(new MiniYaml(pr.Key, pr.Value.Nodes)))
.ToDictionary(player => player.Name);
foreach (var kv in generatedMap.ActorDefinitions)
{
var actorReference = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
var ownerInit = actorReference.Get<OwnerInit>();
if (!players.TryGetValue(ownerInit.InternalName, out var owner))
throw new MapGenerationException("Generator produced mismatching player and actor definitions.");
var preview = new EditorActorPreview(worldRenderer, kv.Key, actorReference, owner);
previews.Add(kv.Key, preview);
}
var blitSource = new EditorBlitSource(generatedMap.AllCells, previews, tiles);
var editorBlit = new EditorBlit(
MapBlitFilters.All,
resourceLayer,
new CPos(0, 0),
map,
blitSource,
editorActorLayer,
false);
var description = FluentProvider.GetMessage(StrGenerated,
"name", FluentProvider.GetMessage(selectedGenerator.Info.Name));
var action = new RandomMapEditorAction(editorBlit, description);
editorActionManager.Add(action);
}
}
}

View File

@@ -10,7 +10,9 @@
#endregion
using System.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Traits;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets.Logic
@@ -19,16 +21,20 @@ namespace OpenRA.Mods.Common.Widgets.Logic
{
[FluentReference]
const string MarkerTiles = "label-tool-marker-tiles";
[FluentReference]
const string MapGenerator = "label-tool-map-generator";
enum MapTool
{
MarkerTiles
MarkerTiles,
MapGenerator
}
readonly DropDownButtonWidget toolsDropdown;
readonly Dictionary<MapTool, string> toolNames = new()
{
{ MapTool.MarkerTiles, MarkerTiles }
{ MapTool.MarkerTiles, MarkerTiles },
{ MapTool.MapGenerator, MapGenerator }
};
readonly Dictionary<MapTool, Widget> toolPanels = new();
@@ -42,10 +48,17 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var markerToolPanel = widget.Get("MARKER_TOOL_PANEL");
toolPanels.Add(MapTool.MarkerTiles, markerToolPanel);
if (world.WorldActor.TraitsImplementing<IMapGenerator>().Any())
{
var mapGeneratorToolPanel = widget.GetOrNull("MAP_GENERATOR_TOOL_PANEL");
if (mapGeneratorToolPanel != null)
toolPanels.Add(MapTool.MapGenerator, mapGeneratorToolPanel);
}
toolsDropdown.OnMouseDown = _ => ShowToolsDropDown(toolsDropdown);
toolsDropdown.GetText = () => FluentProvider.GetMessage(toolNames[selectedTool]);
toolsDropdown.Disabled = true; // TODO: Enable if new tools are added
if (toolPanels.Count <= 1)
toolsDropdown.Disabled = true;
}
void ShowToolsDropDown(DropDownButtonWidget dropdown)
@@ -61,7 +74,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
return item;
}
var options = new[] { MapTool.MarkerTiles };
var options = new[] { MapTool.MarkerTiles, MapTool.MapGenerator };
dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 150, options, SetupItem);
}

View File

@@ -0,0 +1,66 @@
#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.Collections.Generic;
using NUnit.Framework;
using OpenRA.Primitives;
namespace OpenRA.Test
{
[TestFixture]
sealed class PriorityArrayTest
{
[TestCase(0)]
[TestCase(5)]
[TestCase(int.MinValue)]
[TestCase(int.MaxValue)]
public void PriorityArraySequentialTest(int initialValue)
{
var input = new KeyValuePair<int, int>[]
{
new(0, 1),
new(1, 5),
new(2, 3),
new(3, 2),
new(4, 8),
new(5, 7),
new(6, 4),
new(7, 6)
};
var expected = new KeyValuePair<int, int>[]
{
new(0, 1),
new(3, 2),
new(2, 3),
new(6, 4),
new(1, 5),
new(7, 6),
new(5, 7),
new(4, 8)
};
var pa = new PriorityArray<int>(8, initialValue);
foreach (var kv in input)
pa[kv.Key] = kv.Value;
var readback = new KeyValuePair<int, int>[8];
for (var i = 0; i < 8; i++)
{
var index = pa.GetMinIndex();
readback[i] = new KeyValuePair<int, int>(index, pa[index]);
pa[index] = int.MaxValue;
}
Assert.That(readback, Is.EquivalentTo(expected));
}
}
}

View File

@@ -579,6 +579,85 @@ Container@EDITOR_WORLD_ROOT:
Height: 25
Align: Left
Visible: false
ScrollPanel@MAP_GENERATOR_TOOL_PANEL:
Y: 25
Width: PARENT_WIDTH
Height: PARENT_HEIGHT - 25
Visible: false
ScrollBar: Hidden
ScrollbarWidth: 0
Logic: MapGeneratorToolLogic
Children:
LabelForInput@GENERATOR_LABEL:
Y: 5
Width: 65
Height: 25
Align: Right
Font: TinyBold
Text: label-map-generator-generator
For: GENERATOR
DropDownButton@GENERATOR:
X: 70
Y: 5
Width: PARENT_WIDTH - 75
Height: 25
Font: Bold
Button@GENERATE_BUTTON:
X: 5
Y: 45
Width: 95
Height: 25
Text: button-map-generator-generate
Font: Bold
Button@GENERATE_RANDOM_BUTTON:
X: 105
Y: 45
Width: 180
Height: 25
Text: button-map-generator-generate-random
Font: Bold
ScrollPanel@SETTINGS_PANEL:
X: 0
Y: 85
Width: PARENT_WIDTH
Height: PARENT_HEIGHT - 85
Children:
Container@CHECKBOX_TEMPLATE:
X: 5
Width: PARENT_WIDTH - 35
Height: 30
Children:
Checkbox@CHECKBOX:
Width: PARENT_WIDTH
Height: 25
Container@TEXT_TEMPLATE:
X: 5
Width: PARENT_WIDTH - 35
Height: 50
Children:
LabelForInput@LABEL:
Y: 0
Width: PARENT_WIDTH
Height: 20
For: INPUT
TextField@INPUT:
Y: 20
Width: PARENT_WIDTH
Height: 25
Container@DROPDOWN_TEMPLATE:
X: 5
Width: PARENT_WIDTH - 35
Height: 50
Children:
LabelForInput@LABEL:
Y: 0
Width: PARENT_WIDTH
Height: 20
For: DROPDOWN
DropDownButton@DROPDOWN:
Y: 20
Width: PARENT_WIDTH
Height: 25
Container@HISTORY_WIDGETS:
X: WINDOW_WIDTH - 295
Y: 318

View File

@@ -76,6 +76,9 @@ label-marker-layer-num-sides = Number of Sides
label-marker-alpha = Tile Alpha
label-marker-mirror-mode = Mirror Mode
label-marker-axis-angle = Axis Angle
label-map-generator-generator = Generator
button-map-generator-generate = Generate
button-map-generator-generate-random = Generate Random
button-map-editor-tab-container-select-tooltip = Selection
button-map-editor-tab-container-tiles-tooltip = Tiles
@@ -113,6 +116,7 @@ button-select-categories-buttons-all = All
button-select-categories-buttons-none = None
label-tool-marker-tiles = Marker Tiles
label-tool-map-generator = Map Generator
## encyclopedia.yaml, mainmenu.yaml
label-encyclopedia-title = EVA Database

View File

@@ -541,6 +541,86 @@ Container@EDITOR_WORLD_ROOT:
Height: 25
Align: Left
Visible: false
ScrollPanel@MAP_GENERATOR_TOOL_PANEL:
X: 9
Y: 35
Width: 290
Height: WINDOW_HEIGHT - 490
Visible: false
ScrollBar: Hidden
ScrollbarWidth: 0
Logic: MapGeneratorToolLogic
Children:
LabelForInput@GENERATOR_LABEL:
Y: 5
Width: 65
Height: 25
Align: Right
Font: TinyBold
Text: label-map-generator-generator
For: GENERATOR
DropDownButton@GENERATOR:
X: 70
Y: 5
Width: PARENT_WIDTH - 75
Height: 25
Font: Bold
Button@GENERATE_BUTTON:
X: 5
Y: 45
Width: 95
Height: 25
Text: button-map-generator-generate
Font: Bold
Button@GENERATE_RANDOM_BUTTON:
X: 105
Y: 45
Width: 180
Height: 25
Text: button-map-generator-generate-random
Font: Bold
ScrollPanel@SETTINGS_PANEL:
X: 0
Y: 85
Width: PARENT_WIDTH
Height: PARENT_HEIGHT - 85
Children:
Container@CHECKBOX_TEMPLATE:
X: 5
Width: PARENT_WIDTH - 35
Height: 30
Children:
Checkbox@CHECKBOX:
Width: PARENT_WIDTH
Height: 25
Container@TEXT_TEMPLATE:
X: 5
Width: PARENT_WIDTH - 35
Height: 50
Children:
LabelForInput@LABEL:
Y: 0
Width: PARENT_WIDTH
Height: 20
For: INPUT
TextField@INPUT:
Y: 20
Width: PARENT_WIDTH
Height: 25
Container@DROPDOWN_TEMPLATE:
X: 5
Width: PARENT_WIDTH - 35
Height: 50
Children:
LabelForInput@LABEL:
Y: 0
Width: PARENT_WIDTH
Height: 20
For: DROPDOWN
DropDownButton@DROPDOWN:
Y: 20
Width: PARENT_WIDTH
Height: 25
Container@HISTORY_WIDGETS:
X: WINDOW_WIDTH - 320
Y: 354

View File

@@ -72,6 +72,9 @@ label-marker-layer-num-sides = Number of Sides
label-marker-alpha = Tile Alpha
label-marker-mirror-mode = Mirror Mode
label-marker-axis-angle = Axis Angle
label-map-generator-generator = Generator
button-map-generator-generate = Generate
button-map-generator-generate-random = Generate Random
button-map-editor-tab-container-select-tooltip = Selection
button-map-editor-tab-container-tiles-tooltip = Tiles
@@ -113,6 +116,7 @@ button-select-categories-buttons-all = All
button-select-categories-buttons-none = None
label-tool-marker-tiles = Marker Tiles
label-tool-map-generator = Map Generator
## gamesave-browser.yaml
label-gamesave-browser-panel-load-title = Load game

View File

@@ -1120,3 +1120,9 @@ keycode =
.sleep = Sleep
.mouse4 = Mouse 4
.mouse5 = Mouse 5
## MapGeneratorToolLogic
label-map-generator-failed-cancel = Dismiss
notification-map-generator-generated = Generated using { $name }
notification-map-generator-bad-option = Option "{ $option }" is invalid
notification-map-generator-failed = Map generation failed

View File

@@ -34,6 +34,9 @@ options-starting-units =
resource-minerals = Valuable Minerals
map-generator-ra = RA Experimental
map-generator-clear = Clear
## Faction
faction-allies =
.name = Allies
@@ -941,3 +944,71 @@ bot-turtle-ai =
bot-naval-ai =
.name = Naval AI
## map-generators.yaml
label-clear-map-generator-option-tile = Tile
label-clear-map-generator-choice-tile-clear = Clear
label-clear-map-generator-choice-tile-water = Water
label-clear-map-generator-choice-tile-empty = Empty space
label-ra-map-generator-option-seed = Seed
label-ra-map-generator-option-terrain-type = Terrain Type
label-ra-map-generator-choice-terrain-type-lakes = Lakes
label-ra-map-generator-choice-terrain-type-puddles = Puddles
label-ra-map-generator-choice-terrain-type-gardens = Gardens
label-ra-map-generator-choice-terrain-type-plains = Plains
label-ra-map-generator-choice-terrain-type-parks = Parks
label-ra-map-generator-choice-terrain-type-woodlands = Woodlands
label-ra-map-generator-choice-terrain-type-overgrown = Overgrown
label-ra-map-generator-choice-terrain-type-rocky = Rocky
label-ra-map-generator-choice-terrain-type-mountains = Mountains
label-ra-map-generator-choice-terrain-type-mountain-lakes = Mountain Lakes
label-ra-map-generator-choice-terrain-type-oceanic = Oceanic
label-ra-map-generator-choice-terrain-type-large-islands = Large Islands
label-ra-map-generator-choice-terrain-type-continents = Continents
label-ra-map-generator-choice-terrain-type-wetlands = Wetlands
label-ra-map-generator-choice-terrain-type-narrow-wetlands = Narrow Wetlands
label-ra-map-generator-option-rotations = Rotations
label-ra-map-generator-option-mirror = Mirror
label-ra-map-generator-choice-mirror-none = None
label-ra-map-generator-choice-mirror-left-matches-right = Left vs right
label-ra-map-generator-choice-mirror-top-left-matches-bottom-right = Top left vs bottom right
label-ra-map-generator-choice-mirror-top-matches-bottom = Top vs bottom
label-ra-map-generator-choice-mirror-top-right-matches-bottom-left = Top right vs bottom left
label-ra-map-generator-option-shape = Bounds Shape
label-ra-map-generator-choice-shape-square = Square
label-ra-map-generator-choice-shape-circle-mountain = Circle in mountains
label-ra-map-generator-choice-shape-circle-water = Circle in water
label-ra-map-generator-option-players = Players per side
label-ra-map-generator-option-resources = Resources
label-ra-map-generator-choice-resources-none = None
label-ra-map-generator-choice-resources-low = Low
label-ra-map-generator-choice-resources-medium = Medium
label-ra-map-generator-choice-resources-high = High
label-ra-map-generator-choice-resources-very-high = Very High
label-ra-map-generator-choice-resources-full = Oreful
label-ra-map-generator-option-buildings = Buildings
label-ra-map-generator-choice-buildings-none = None
label-ra-map-generator-choice-buildings-standard = Standard
label-ra-map-generator-choice-buildings-extra = Extra
label-ra-map-generator-choice-buildings-oil-only = Oil Only
label-ra-map-generator-choice-buildings-oil-rush = Oil Rush
label-ra-map-generator-option-density = Entity Density
label-ra-map-generator-choice-density-players = Scale with players
label-ra-map-generator-choice-density-area-and-players = Scale with area and players
label-ra-map-generator-choice-density-area-very-low = Scale with area (very low density)
label-ra-map-generator-choice-density-area-low = Scale with area (low density)
label-ra-map-generator-choice-density-area-medium = Scale with area (medium density)
label-ra-map-generator-choice-density-area-high = Scale with area (high density)
label-ra-map-generator-choice-density-area-very-high = Scale with area (very high density)
label-ra-map-generator-option-roads = Roads
label-ra-map-generator-option-deny-walled-areas = Obstruct walled areas

View File

@@ -86,6 +86,7 @@ Rules:
ra|rules/aircraft.yaml
ra|rules/ships.yaml
ra|rules/fakes.yaml
ra|rules/map-generators.yaml
Sequences:
ra|sequences/ships.yaml

View File

@@ -0,0 +1,431 @@
^MapGenerators:
RaMapGenerator@ra:
Type: ra
Name: map-generator-ra
Settings:
Option@hidden_defaults:
Choice@hidden_defaults:
Settings:
TerrainFeatureSize: 20.0
ForestFeatureSize: 20.0
ResourceFeatureSize: 20.0
Water: 0.2
Mountains: 0.1
Forests: 0.025
ForestCutout: 2
MaximumCutoutSpacing: 12
TerrainSmoothing: 4
SmoothingThreshold: 0.833333
MinimumLandSeaThickness: 5
MinimumMountainThickness: 5
MaximumAltitude: 8
RoughnessRadius: 5
Roughness: 0.5
MinimumTerrainContourSpacing: 6
MinimumCliffLength: 10
ForestClumpiness: 0.5
DenyWalledAreas: True
EnforceSymmetry: 0
Roads: True
RoadSpacing: 5
RoadShrink: 0
CreateEntities: True
CentralSpawnReservationFraction: 0.25
ResourceSpawnReservation: 8
SpawnRegionSize: 12
SpawnBuildSize: 8
SpawnResourceSpawns: 3
SpawnReservation: 20
SpawnResourceBias: 1.15
ResourcesPerPlayer: 50000
OreUniformity: 0.25
OreClumpiness: 0.25
MaximumExpansionResourceSpawns: 5
MaximumResourceSpawnsPerExpansion: 2
MinimumExpansionSize: 2
MaximumExpansionSize: 12
ExpansionInner: 2
ExpansionBorder: 1
DefaultResource: Ore
ResourceSpawnSeeds:
mine: Ore
gmine: Gems
ClearTerrain: Clear
PlayableTerrain: Beach,Bridge,Clear,Gems,Ore,Road,Rough,Wall,Water
PartiallyPlayableTerrain: Tree
UnplayableTerrain: River,Rock
DominantTerrain: River,Rock,Tree,Water
PartiallyPlayableCategories: Beach,Road
ClearSegmentTypes: Clear
BeachSegmentTypes: Beach
CliffSegmentTypes: Cliff
RoadSegmentTypes: Road,RoadIn,RoadOut
ForestObstacles: Trees
UnplayableObstacles: Obstructions
Option@hidden_tileset_overrides:
Choice@desert:
Tileset: DESERT
Settings:
LandTile: 255
WaterTile: 256
Choice@temperat:
Tileset: TEMPERAT
Settings:
LandTile: 255
WaterTile: 1
Choice@snow:
Tileset: SNOW
Settings:
LandTile: 255
WaterTile: 1
Option@Seed:
Label: label-ra-map-generator-option-seed
Random: True
Default: 0
Integer: Seed
Option@TerrainType:
Label: label-ra-map-generator-option-terrain-type
Priority: 2
Default: Gardens
Choice@Lakes:
Label: label-ra-map-generator-choice-terrain-type-lakes
Settings:
Choice@Puddles:
Label: label-ra-map-generator-choice-terrain-type-puddles
Settings:
Water: 0.1
Choice@Gardens:
Label: label-ra-map-generator-choice-terrain-type-gardens
Settings:
Water: 0.05
Forests: 0.3
ForestCutout: 3
EnforceSymmetry: 2
RoadSpacing: 3
RoadShrink: 4
Choice@Plains:
Label: label-ra-map-generator-choice-terrain-type-plains
Settings:
Water: 0.0
Choice@Parks:
Label: label-ra-map-generator-choice-terrain-type-parks
Settings:
Water: 0.0
Forests: 0.1
Choice@Woodlands:
Label: label-ra-map-generator-choice-terrain-type-woodlands
Settings:
Water: 0.0
Forests: 0.4
ForestCutout: 3
EnforceSymmetry: 2
RoadSpacing: 3
RoadShrink: 4
Choice@Overgrown:
Label: label-ra-map-generator-choice-terrain-type-overgrown
Settings:
Water: 0.0
Forests: 0.5
EnforceSymmetry: 2
Mountains: 0.5
Roughness: 0.25
Choice@Rocky:
Label: label-ra-map-generator-choice-terrain-type-rocky
Settings:
Water: 0.0
Forests: 0.3
ForestCutout: 3
EnforceSymmetry: 2
Mountains: 0.5
Roughness: 0.25
RoadSpacing: 3
RoadShrink: 4
Choice@Mountains:
Label: label-ra-map-generator-choice-terrain-type-mountains
Settings:
Water: 0.0
Mountains: 1.0
Roughness: 0.60
MinimumTerrainContourSpacing: 5
Choice@MountainLakes:
Label: label-ra-map-generator-choice-terrain-type-mountain-lakes
Settings:
Water: 0.2
Mountains: 1.0
Roughness: 0.85
MinimumTerrainContourSpacing: 5
Choice@Oceanic:
Label: label-ra-map-generator-choice-terrain-type-oceanic
Settings:
Water: 0.8
Forests: 0.0
Choice@LargeIslands:
Label: label-ra-map-generator-choice-terrain-type-large-islands
Settings:
Water: 0.75
TerrainFeatureSize: 50.0
Forests: 0.0
Choice@Continents:
Label: label-ra-map-generator-choice-terrain-type-continents
Settings:
Water: 0.5
TerrainFeatureSize: 100.0
Choice@Wetlands:
Label: label-ra-map-generator-choice-terrain-type-wetlands
Settings:
Water: 0.5
Choice@NarrowWetlands:
Label: label-ra-map-generator-choice-terrain-type-narrow-wetlands
Settings:
Water: 0.5
TerrainFeatureSize: 5.0
Forests: 0.0
SpawnBuildSize: 6
Option@Rotations:
Label: label-ra-map-generator-option-rotations
SimpleChoice: Rotations
Values: 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16
Default: 2
Priority: 1
Option@Mirror:
Label: label-ra-map-generator-option-mirror
Default: None
Priority: 1
Choice@None:
Label: label-ra-map-generator-choice-mirror-none
Settings:
Mirror: None
Choice@LeftMatchesRight:
Label: label-ra-map-generator-choice-mirror-left-matches-right
Settings:
Mirror: LeftMatchesRight
Choice@TopLeftMatchesBottomRight:
Label: label-ra-map-generator-choice-mirror-top-left-matches-bottom-right
Settings:
Mirror: TopLeftMatchesBottomRight
Choice@TopMatchesBottom:
Label: label-ra-map-generator-choice-mirror-top-matches-bottom
Settings:
Mirror: TopMatchesBottom
Choice@TopRightMatchesBottomLeft:
Label: label-ra-map-generator-choice-mirror-top-right-matches-bottom-left
Settings:
Mirror: TopRightMatchesBottomLeft
Option@Shape:
Label: label-ra-map-generator-option-shape
Default: Square
Priority: 1
Choice@Square:
Label: label-ra-map-generator-choice-shape-square
Settings:
ExternalCircularBias: 0
Choice@CircleMountain:
Label: label-ra-map-generator-choice-shape-circle-mountain
Settings:
ExternalCircularBias: 1
Choice@CircleWater:
Label: label-ra-map-generator-choice-shape-circle-water
Settings:
ExternalCircularBias: -1
Option@Players:
Label: label-ra-map-generator-option-players
SimpleChoice: Players
Values: 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16
Default: 1
Priority: 1
Option@Resources:
Label: label-ra-map-generator-option-resources
Default: Medium
Choice@None:
Label: label-ra-map-generator-choice-resources-none
Settings:
SpawnResourceSpawns: 0
ResourcesPerPlayer: 0
ResourceSpawnWeights:
MaximumExpansionResourceSpawns: 0
MaximumResourceSpawnsPerExpansion: 1
Choice@Low:
Label: label-ra-map-generator-choice-resources-low
Settings:
SpawnResourceSpawns: 2
ResourcesPerPlayer: 25000
ResourceSpawnWeights:
mine: 1.0
MaximumExpansionResourceSpawns: 3
MaximumResourceSpawnsPerExpansion: 1
Choice@Medium:
Label: label-ra-map-generator-choice-resources-medium
Settings:
SpawnResourceSpawns: 3
ResourcesPerPlayer: 50000
ResourceSpawnWeights:
mine: 0.95
gmine: 0.05
MaximumExpansionResourceSpawns: 5
MaximumResourceSpawnsPerExpansion: 2
Choice@High:
Label: label-ra-map-generator-choice-resources-high
Settings:
SpawnResourceSpawns: 3
ResourcesPerPlayer: 75000
ResourceSpawnWeights:
mine: 0.9
gmine: 0.1
MaximumExpansionResourceSpawns: 8
MaximumResourceSpawnsPerExpansion: 3
Choice@VeryHigh:
Label: label-ra-map-generator-choice-resources-very-high
Settings:
SpawnResourceSpawns: 4
ResourcesPerPlayer: 100000
ResourceSpawnWeights:
mine: 0.8
gmine: 0.2
MaximumExpansionResourceSpawns: 10
MaximumResourceSpawnsPerExpansion: 3
Choice@Full:
Label: label-ra-map-generator-choice-resources-full
Settings:
SpawnResourceSpawns: 0
ResourcesPerPlayer: 1000000000
ResourceSpawnWeights:
MaximumExpansionResourceSpawns: 0
MaximumResourceSpawnsPerExpansion: 1
Option@Buildings:
Label: label-ra-map-generator-option-buildings
Default: Standard
Choice@None:
Label: label-ra-map-generator-choice-buildings-none
Settings:
MinimumBuildings: 0
MaximumBuildings: 0
BuildingWeights:
Choice@Standard:
Label: label-ra-map-generator-choice-buildings-standard
Settings:
MinimumBuildings: 0
MaximumBuildings: 3
BuildingWeights:
hosp: 2
miss: 1
oilb: 9
Choice@Extra:
Label: label-ra-map-generator-choice-buildings-extra
Settings:
MinimumBuildings: 3
MaximumBuildings: 6
BuildingWeights:
fcom: 3
hosp: 2
miss: 1
oilb: 9
pbox: 2
Choice@OilOnly:
Label: label-ra-map-generator-choice-buildings-oil-only
Settings:
MinimumBuildings: 0
MaximumBuildings: 3
BuildingWeights:
oilb: 1
Choice@OilRush:
Label: label-ra-map-generator-choice-buildings-oil-rush
Settings:
MinimumBuildings: 8
MaximumBuildings: 10
BuildingWeights:
oilb: 1
Option@Density:
Label: label-ra-map-generator-option-density
Default: Players
Priority: 1
Choice@Players:
Label: label-ra-map-generator-choice-density-players
Settings:
AreaEntityBonus: 0.0
PlayerCountEntityBonus: 1.0
Choice@AreaAndPlayers:
Label: label-ra-map-generator-choice-density-area-and-players
Settings:
AreaEntityBonus: 0.0002
PlayerCountEntityBonus: 0.5
Choice@AreaVeryLow:
Label: label-ra-map-generator-choice-density-area-very-low
Settings:
AreaEntityBonus: 0.0001
PlayerCountEntityBonus: 0.0
Choice@AreaLow:
Label: label-ra-map-generator-choice-density-area-low
Settings:
AreaEntityBonus: 0.0002
PlayerCountEntityBonus: 0.0
Choice@AreaMedium:
Label: label-ra-map-generator-choice-density-area-medium
Settings:
AreaEntityBonus: 0.0004
PlayerCountEntityBonus: 0.0
Choice@AreaHigh:
Label: label-ra-map-generator-choice-density-area-high
Settings:
AreaEntityBonus: 0.0006
PlayerCountEntityBonus: 0.0
Choice@AreaVeryHigh:
Label: label-ra-map-generator-choice-density-area-very-high
Settings:
AreaEntityBonus: 0.0008
PlayerCountEntityBonus: 0.0
Option@DenyWalledArea:
Label: label-ra-map-generator-option-deny-walled-areas
Checkbox: DenyWalledAreas
Default: True
Priority: 1
Option@Roads:
Label: label-ra-map-generator-option-roads
Checkbox: Roads
Default: True
Priority: 1
ClearMapGenerator@clear:
Type: clear
Name: map-generator-clear
Settings:
Option@Tile:
Label: label-clear-map-generator-option-tile
Choice@DesertClear:
Label: label-clear-map-generator-choice-tile-clear
Tileset: DESERT
Settings:
Tile: 255
Choice@DesertWater:
Label: label-clear-map-generator-choice-tile-water
Tileset: DESERT
Settings:
Tile: 256
Choice@SnowClear:
Label: label-clear-map-generator-choice-tile-clear
Tileset: SNOW
Settings:
Tile: 255
Choice@SnowWater:
Label: label-clear-map-generator-choice-tile-water
Tileset: SNOW
Settings:
Tile: 1
Choice@TemperatClear:
Label: label-clear-map-generator-choice-tile-clear
Tileset: TEMPERAT
Settings:
Tile: 255
Choice@TemperatWater:
Label: label-clear-map-generator-choice-tile-water
Tileset: TEMPERAT
Settings:
Tile: 1
Choice@InteriorClear:
Label: label-clear-map-generator-choice-tile-clear
Tileset: INTERIOR
Settings:
Tile: 275
Choice@InteriorEmpty:
Label: label-clear-map-generator-choice-tile-empty
Tileset: INTERIOR
Settings:
Tile: 255

View File

@@ -298,6 +298,7 @@ World:
EditorWorld:
Inherits: ^BaseWorld
Inherits@MapGenerators: ^MapGenerators
EditorActorLayer:
EditorCursorLayer:
EditorResourceLayer:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff