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:
1
AUTHORS
1
AUTHORS
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
130
OpenRA.Game/Primitives/PriorityArray.cs
Normal file
130
OpenRA.Game/Primitives/PriorityArray.cs
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
51
OpenRA.Mods.Common/Lint/CheckMultiBrushes.cs
Normal file
51
OpenRA.Mods.Common/Lint/CheckMultiBrushes.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
149
OpenRA.Mods.Common/MapGenerator/ActorPlan.cs
Normal file
149
OpenRA.Mods.Common/MapGenerator/ActorPlan.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
500
OpenRA.Mods.Common/MapGenerator/CellLayerUtils.cs
Normal file
500
OpenRA.Mods.Common/MapGenerator/CellLayerUtils.cs
Normal 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 <= 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
296
OpenRA.Mods.Common/MapGenerator/Direction.cs
Normal file
296
OpenRA.Mods.Common/MapGenerator/Direction.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
364
OpenRA.Mods.Common/MapGenerator/MapGeneratorSettings.cs
Normal file
364
OpenRA.Mods.Common/MapGenerator/MapGeneratorSettings.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
186
OpenRA.Mods.Common/MapGenerator/Matrix.cs
Normal file
186
OpenRA.Mods.Common/MapGenerator/Matrix.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
1412
OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs
Normal file
1412
OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs
Normal file
File diff suppressed because it is too large
Load Diff
455
OpenRA.Mods.Common/MapGenerator/MultiBrush.cs
Normal file
455
OpenRA.Mods.Common/MapGenerator/MultiBrush.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
177
OpenRA.Mods.Common/MapGenerator/NoiseUtils.cs
Normal file
177
OpenRA.Mods.Common/MapGenerator/NoiseUtils.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
148
OpenRA.Mods.Common/MapGenerator/PlayableSpace.cs
Normal file
148
OpenRA.Mods.Common/MapGenerator/PlayableSpace.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
319
OpenRA.Mods.Common/MapGenerator/Symmetry.cs
Normal file
319
OpenRA.Mods.Common/MapGenerator/Symmetry.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
1195
OpenRA.Mods.Common/MapGenerator/TilingPath.cs
Normal file
1195
OpenRA.Mods.Common/MapGenerator/TilingPath.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
{
|
||||
|
||||
63
OpenRA.Mods.Common/Terrain/TemplateSegment.cs
Normal file
63
OpenRA.Mods.Common/Terrain/TemplateSegment.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
115
OpenRA.Mods.Common/Traits/World/ClearMapGenerator.cs
Normal file
115
OpenRA.Mods.Common/Traits/World/ClearMapGenerator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
1603
OpenRA.Mods.Common/Traits/World/RaMapGenerator.cs
Normal file
1603
OpenRA.Mods.Common/Traits/World/RaMapGenerator.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
355
OpenRA.Mods.Common/Widgets/Logic/Editor/MapGeneratorToolLogic.cs
Normal file
355
OpenRA.Mods.Common/Widgets/Logic/Editor/MapGeneratorToolLogic.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
66
OpenRA.Test/OpenRA.Game/PriorityArrayTest.cs
Normal file
66
OpenRA.Test/OpenRA.Game/PriorityArrayTest.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
431
mods/ra/rules/map-generators.yaml
Normal file
431
mods/ra/rules/map-generators.yaml
Normal 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
|
||||
@@ -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
Reference in New Issue
Block a user