Refactor ExperimentalMapGenerator into reusable methods

This change lifts large portions of ExperimentalMapGenerator's logic
into a new "Terraformer" class with highly documented methods that can
theoretically be used by alternative map generator classes. Some
additional refactoring may occur in future, subject to the practical
needs of additional map generators. ClearMapGenerator is also
simplified.

This is not a pure refactor and contains some algorithmic and
behavioral changes, as well as few minor bug fixes. Notably:

- The logic for obstructing unreachable water has been somewhat
  replaced.

- In CnC, where water is already unplayable, EMG no longer obstructs
  water. (RA still does, as water is playable there.)

- Mountain (cliff) generation is now somewhat more effective. This
  increases the number of cliffs seen on many presets. Settings should
  no longer use "Mountains: 1000".

- PlayableSpace logic has been consolidated into Terraformer.

- PlayableSpace.Playability no longer exists as PartiallyPlayable
  became redundant. Its uses have been replaced with a simple
  boolean. Consequently, some defunct code and configuration has been
  removed.

- Adjust MultiBrush replaceability contract painting behavior to be
  more intuitive.

- Fix off-by-one error in map bounds computation.

- Fix some usages of mixed up MersenneTwisters.
This commit is contained in:
Ashley Newson
2025-06-26 22:41:58 +01:00
committed by Gustas Kažukauskas
parent e4f289921c
commit 25571df2b6
12 changed files with 2534 additions and 1480 deletions

View File

@@ -145,5 +145,16 @@ namespace OpenRA.Mods.Common.MapGenerator
return CellLayerUtils.CVecToWVec(new CVec(left + right, top + bottom), Map.Grid.Type) / 2;
}
/// <summary>Return the larger of the width or height of the actor's footprint.</summary>
public int MaxSpan()
{
var footprint = Footprint().Select(kv => kv.Key).ToList();
var minX = footprint.Min(cpos => cpos.X);
var minY = footprint.Min(cpos => cpos.Y);
var maxX = footprint.Max(cpos => cpos.X);
var maxY = footprint.Max(cpos => cpos.Y);
return Math.Max(maxX - minX, maxY - minY) + 1;
}
}
}

View File

@@ -60,6 +60,25 @@ namespace OpenRA.Mods.Common.MapGenerator
}
}
public static WPos Center(Map map)
{
return Center(map.Tiles);
}
/// <summary>
/// Return the radius of the largest circle that can be contained in the cell layer.
/// </summary>
public static WDist Radius<T>(CellLayer<T> cellLayer)
{
var center = Center(cellLayer);
return new WDist(Math.Min(center.X, center.Y));
}
public static WDist Radius(Map map)
{
return Radius(map.Tiles);
}
/// <summary>Get the WPos of the -X-Y corner of a CPos cell.</summary>
public static WPos CornerToWPos(CPos cpos, MapGridType gridType)
{
@@ -218,7 +237,7 @@ namespace OpenRA.Mods.Common.MapGenerator
public static void OverCircle<T>(
CellLayer<T> cellLayer,
WPos wCenter,
int wRadius,
WDist wRadius,
bool outside,
Action<MPos, CPos, WPos, long> action)
{
@@ -243,12 +262,12 @@ namespace OpenRA.Mods.Common.MapGenerator
switch (gridType)
{
case MapGridType.Rectangular:
mRadiusU = wRadius / 1024 + 1;
mRadiusV = wRadius / 1024 + 1;
mRadiusU = wRadius.Length / 1024 + 1;
mRadiusV = wRadius.Length / 1024 + 1;
break;
case MapGridType.RectangularIsometric:
mRadiusU = wRadius / 1448 + 2;
mRadiusV = wRadius / 724 + 2;
mRadiusU = wRadius.Length / 1448 + 2;
mRadiusV = wRadius.Length / 724 + 2;
break;
default:
throw new NotImplementedException();
@@ -260,7 +279,7 @@ namespace OpenRA.Mods.Common.MapGenerator
maxV = Math.Min(mCenter.V + mRadiusV, cellLayer.Size.Height - 1);
}
var wRadiusSquared = (long)wRadius * wRadius;
var wRadiusSquared = wRadius.LengthSquared;
for (var v = minV; v <= maxV; v++)
for (var u = minU; u <= maxU; u++)
{
@@ -288,8 +307,10 @@ namespace OpenRA.Mods.Common.MapGenerator
}
/// <summary>
/// Uniformally add to or subtract from all cells such that count out of every outOf cells,
/// are no greater than the given target value.
/// Uniformally add to or subtract from all cells such that the quantile (count/outOf) has at the target value.
/// For example, (target: 0, count: 25, outOf: 75) where there are 401 cells would mean
/// that 100 cells are no greater than 0, 300 cells are no less than 0, and at least 1 cell
/// is 0.
/// </summary>
public static void CalibrateQuantileInPlace(CellLayer<int> cellLayer, int target, int count, int outOf)
{
@@ -300,6 +321,32 @@ namespace OpenRA.Mods.Common.MapGenerator
cellLayer[mpos] += adjustment;
}
/// <summary>
/// Return a boolean CellLayer where true correlates with the largest values in the input,
/// such that the fraction of true cells is at least (but approximately) count/outOf.
/// </summary>
public static CellLayer<bool> CalibratedBooleanThreshold(CellLayer<int> input, int count, int outOf)
{
var output = new CellLayer<bool>(input.GridType, input.Size);
if (count <= 0)
{
return output;
}
else if (count >= outOf)
{
output.Clear(true);
return output;
}
var sorted = Entries(input);
Array.Sort(sorted);
var threshold = sorted[(long)sorted.Length * (outOf - count) / outOf];
foreach (var mpos in input.CellRegion.MapCoords)
output[mpos] = input[mpos] >= threshold;
return output;
}
/// <summary>
/// Get the smallest CPos rectangle that contains all cells for the specified grid.
/// </summary>
@@ -419,10 +466,10 @@ namespace OpenRA.Mods.Common.MapGenerator
/// Returns world distances (1024ths).
/// </summary>
public static void WalkingDistances(
CellLayer<int> distances,
CellLayer<WDist> distances,
CellLayer<bool> passable,
IEnumerable<CPos> seeds,
int maxDistance)
WDist maxDistance)
{
var passableMatrix = ToMatrix(passable, false);
var cellBounds = CellBounds(passable);
@@ -532,5 +579,111 @@ namespace OpenRA.Mods.Common.MapGenerator
}
}
}
/// <summary>
/// Simple flood fill that propagates, starting from seed cells, throughout a masked area.
/// The fillAction is run once (in a consistent order) for each filled cell.
/// </summary>
public static void SimpleFloodFill(
CellLayer<bool> mask,
CellLayer<bool> seeds,
Action<CPos> fillAction,
ImmutableArray<CVec> spread)
{
if (!AreSameShape(mask, seeds))
throw new ArgumentException("mask and seeds did not have same shape");
var available = Clone(mask);
bool? Filler(CPos cpos, bool _)
{
if (!available[cpos])
return null;
fillAction(cpos);
available[cpos] = false;
return true;
}
FloodFill(
available,
seeds.CellRegion
.Where(cpos => seeds[cpos] && mask[cpos])
.Select(cpos => (cpos, true)),
Filler,
spread);
}
/// <summary>Return logical AND / conjunction / intersection of layers.</summary>
public static CellLayer<bool> Intersect(IEnumerable<CellLayer<bool>> layers)
{
return Aggregate(layers, (a, b) => a && b);
}
/// <summary>
/// Return the difference of layers. Each cell is true if and only if something appears
/// only in the first layer.
/// </summary>
public static CellLayer<bool> Subtract(IEnumerable<CellLayer<bool>> layers)
{
return Aggregate(layers, (a, b) => a && !b);
}
public static CellLayer<T> Aggregate<T>(
IEnumerable<CellLayer<T>> layers,
Func<T, T, T> aggregator)
{
var layersArray = layers.ToArray();
if (layersArray.Length == 0)
throw new ArgumentException("No layers were supplied");
var accumulator = new CellLayer<T>(layersArray[0].GridType, layersArray[0].Size);
accumulator.CopyValuesFrom(layersArray[0]);
foreach (var layer in layersArray.Skip(1))
{
if (!AreSameShape(accumulator, layer))
throw new ArgumentException("Layers are not the same shape");
foreach (var mpos in accumulator.CellRegion.MapCoords)
accumulator[mpos] = aggregator(accumulator[mpos], layer[mpos]);
}
return accumulator;
}
/// <summary>Create a shallow copy of a CellLayer.</summary>
public static CellLayer<T> Clone<T>(CellLayer<T> input)
{
var output = new CellLayer<T>(input.GridType, input.Size);
output.CopyValuesFrom(input);
return output;
}
public static CellLayer<R> Map<T, R>(CellLayer<T> input, Func<T, R> func)
{
var output = new CellLayer<R>(input.GridType, input.Size);
foreach (var mpos in input.CellRegion.MapCoords)
output[mpos] = func(input[mpos]);
return output;
}
/// <summary>Create and initialize a CellLayer according to the given function.</summary>
public static CellLayer<T> Create<T>(Map map, Func<MPos, T> func)
{
var layer = new CellLayer<T>(map);
foreach (var mpos in map.AllCells.MapCoords)
layer[mpos] = func(mpos);
return layer;
}
/// <summary>Create and initialize a CellLayer according to the given function.</summary>
public static CellLayer<T> Create<T>(Map map, Func<CPos, T> func)
{
var layer = new CellLayer<T>(map);
foreach (var cpos in map.AllCells)
layer[cpos] = func(cpos);
return layer;
}
}
}

View File

@@ -23,6 +23,164 @@ namespace OpenRA.Mods.Common.MapGenerator
{
public const int MaxBinomialKernelRadius = 10;
public enum DumpAdjustment
{
/// <summary>Make no adjustment.</summary>
None,
/// <summary>Normalize the matrix amplitude to the color range.</summary>
Normalize,
/// <summary>
/// Normalize the matrix amplitude, but uniformally extend away from zero by a small
/// amount to help identify the sign of martix values.
/// </summary>
Emphasize,
}
public enum GraphMode
{
/// <summary>
/// The plotted value is the latest sequence touching a cell + 1.
/// </summary>
Identifier,
/// <summary>
/// The plotted value is the (latest) point index in the (latest) sequence touching a
/// cell.
/// </summary>
Gradient,
/// <summary>The plotted value is the count of points touching a cell.</summary>
Accumulate,
}
/// <summary>
/// <para>
/// Debugging method that prints a matrix to stderr using color only (not value listing).
/// </para>
/// <para>
/// Orange &lt; -255, -255 &lt;= Red &lt; 0, Black == 0, 0 &lt; Blue &lt;= 255,
/// 255 &lt; Cyan. Faint green is used for distance markings.
/// </para>
/// <para>
/// The matrix can optionally be preprocessed for easier visual interpretation using a
/// DumpAdjustment.
/// </para>
/// </summary>
public static void ColorDump2d(
string label,
Matrix<int> matrix,
DumpAdjustment adjustment = DumpAdjustment.None)
{
Console.Error.WriteLine($"{label}: {matrix.Size.X} by {matrix.Size.Y}, {matrix.Data.Min()} to {matrix.Data.Max()}");
switch (adjustment)
{
case DumpAdjustment.Normalize:
matrix = NormalizeRangeInPlace(matrix.Clone(), 255);
break;
case DumpAdjustment.Emphasize:
matrix = NormalizeRangeInPlace(matrix.Clone(), 224)
.Map(v => v += Math.Sign(v) * 31);
break;
default:
break;
}
for (var y = 0; y < matrix.Size.Y; y++)
{
for (var x = 0; x < matrix.Size.X; x++)
{
var v = matrix[x, y];
int r = 0, g = 0, b = 0;
if (v < -255)
{
r = 255;
g = 192;
}
else if (v < 0)
{
r = -v;
}
else if (v == 0)
{
}
else if (v <= 255)
{
b = v;
g = v / 4;
}
else
{
// v > 255
b = 255;
g = 192;
}
g += (((x & 4) != (y & 4)) ? 1 : 0) * (((x & 16) != (y & 16)) ? 48 : 32);
Console.Error.Write(string.Format(NumberFormatInfo.InvariantInfo, "\u001b[48;2;{0};{1};{2}m ", r, g, b));
}
Console.Error.Write("\u001b[0m\n");
}
Console.Error.WriteLine("");
Console.Error.Flush();
}
public static void ColorDump2d(
string label,
Matrix<bool> matrix)
{
ColorDump2d(label, matrix.Map(v => v ? 255 : -255));
}
/// <summary>
/// Debugging method that prints a matrix of enum-like values to stderr, where values are
/// mapped to one of 27 different colors. Red, green, and blue values represent base-3
/// digits of increasing significance. Unmappable values produce white. A corresponding
/// letter of the latin alphabet is also written in the right of cells greater than zero.
/// E.g., 21_base10 = 210_base3 = bright blue + medium green + no red, letter U.
/// </summary>
public static void EnumDump2d(string label, Matrix<int> matrix)
{
Console.Error.WriteLine($"{label}: {matrix.Size.X} by {matrix.Size.Y}, {matrix.Data.Min()} to {matrix.Data.Max()}");
for (var y = 0; y < matrix.Size.Y; y++)
{
for (var x = 0; x < matrix.Size.X; x++)
{
var v = matrix[x, y];
if (v < 0 || v > 26)
v = 26;
var r = 127 * (v / 1 % 3);
var g = 127 * (v / 3 % 3);
var b = 127 * (v / 9 % 3);
var f = (r + g + b <= 127) ? 37 : 30;
var c = v > 0 ? (char)(64 + v) : '.';
Console.Error.Write(string.Format(NumberFormatInfo.InvariantInfo, "\u001b[{0};48;2;{1};{2};{3}m {4}", f, r, g, b, c));
// if (v < 0 || v >= 15)
// v = 15;
// var code = (v < 8 ? 40 : 92) + v;
// Console.Error.Write(string.Format(NumberFormatInfo.InvariantInfo, "\u001b[{0}m .", code));
}
Console.Error.Write("\u001b[0m\n");
}
Console.Error.WriteLine("");
Console.Error.Flush();
}
public static void EnumDump2d<T>(string label, Matrix<T> matrix) where T : Enum
{
EnumDump2d(label, matrix.Map(v => Convert.ToInt32(v, NumberFormatInfo.InvariantInfo)));
}
/// <summary>
/// Debugging method that prints a matrix to stderr.
/// </summary>
@@ -96,6 +254,55 @@ namespace OpenRA.Mods.Common.MapGenerator
Console.Error.Flush();
}
/// <summary>
/// Plot multiple point sequences onto a matrix for debugging visualization. The matrix is
/// fit to the shape of all the path.
/// </summary>
public static Matrix<int> GraphPoints(
IEnumerable<IEnumerable<int2>> pointArrays,
GraphMode mode = GraphMode.Identifier)
{
var pointArrayArray = pointArrays.Select(a => a.ToArray()).ToArray();
var allPoints = pointArrayArray.SelectMany(p => p).ToArray();
if (allPoints.Length == 0)
return new Matrix<int>(1, 1).Fill(int.MinValue);
var topLeft = new int2(allPoints.Min(p => p.X), allPoints.Min(p => p.Y));
var bottomRight = new int2(allPoints.Max(p => p.X), allPoints.Max(p => p.Y));
var size = bottomRight - topLeft + new int2(1, 1);
var matrix = new Matrix<int>(size).Fill(mode == GraphMode.Gradient ? -1 : 0);
for (var j = 0; j < pointArrayArray.Length; j++)
{
var pointArray = pointArrayArray[j];
for (var i = 0; i < pointArray.Length; i++)
switch (mode)
{
case GraphMode.Identifier:
matrix[pointArray[i] - topLeft] = j + 1;
break;
case GraphMode.Gradient:
matrix[pointArray[i] - topLeft] = i;
break;
case GraphMode.Accumulate:
matrix[pointArray[i] - topLeft]++;
break;
}
}
return matrix;
}
/// <summary>
/// Plot a point sequence onto a matrix for debugging visualization. The matrix is fit to
/// the shape of the path.
/// </summary>
public static Matrix<int> GraphPoints(
IEnumerable<int2> points,
GraphMode mode = GraphMode.Identifier)
{
return GraphPoints([points], mode);
}
/// <summary>
/// <para>
/// Perform a generic flood fill starting at seeds <c>[(xy, prop), ...]</c>.
@@ -166,12 +373,12 @@ namespace OpenRA.Mods.Common.MapGenerator
/// int.MaxValue.
/// </para>
/// </summary>
public static Matrix<int> WalkingDistances(Matrix<bool> passable, IEnumerable<int2> seeds, int maxDistance)
public static Matrix<WDist> WalkingDistances(Matrix<bool> passable, IEnumerable<int2> seeds, WDist maxDistance)
{
const int Diagonal = 1448;
const int Straight = 1024;
var output = new Matrix<int>(passable.Size).Fill(int.MaxValue);
var output = new Matrix<WDist>(passable.Size).Fill(WDist.MaxValue);
var unprocessed = new PriorityArray<int>(passable.Size.X * passable.Size.Y, int.MaxValue);
foreach (var seed in seeds)
unprocessed[passable.Index(seed)] = 0;
@@ -182,11 +389,11 @@ namespace OpenRA.Mods.Common.MapGenerator
var distance = unprocessed[i];
var xy = passable.XY(i);
if (distance > maxDistance)
if (distance > maxDistance.Length)
break;
if (distance <= maxDistance && output.ContainsXY(xy))
output[xy] = distance;
if (distance <= maxDistance.Length && output.ContainsXY(xy))
output[xy] = new WDist(distance);
unprocessed[i] = int.MaxValue;
foreach (var (offset, direction) in DirectionExts.Spread8D)
@@ -196,7 +403,7 @@ namespace OpenRA.Mods.Common.MapGenerator
continue;
if (!passable[nextXY])
continue;
if (output[nextXY] != int.MaxValue)
if (output[nextXY] != WDist.MaxValue)
continue;
int nextDistance;
if (direction.IsDiagonal())
@@ -712,8 +919,10 @@ namespace OpenRA.Mods.Common.MapGenerator
}
/// <summary>
/// Uniformally add to or subtract from all cells such that count out of every outOf cells,
/// are no greater than the given target value.
/// Uniformally add to or subtract from all cells such that the quantile (count/outOf) has at the target value.
/// For example, (target: 0, count: 25, outOf: 75) where there are 401 cells would mean
/// that 100 cells are no greater than 0, 300 cells are no less than 0, and at least 1 cell
/// is 0.
/// </summary>
public static void CalibrateQuantileInPlace(Matrix<int> matrix, int target, int count, int outOf)
{
@@ -724,6 +933,23 @@ namespace OpenRA.Mods.Common.MapGenerator
matrix[i] += adjustment;
}
/// <summary>
/// Return a boolean matrix where true correlates with the largest values in the input,
/// such that the fraction of true cells is at least (but approximately) count/outOf.
/// </summary>
public static Matrix<bool> CalibratedBooleanThreshold(Matrix<int> input, int count, int outOf)
{
if (count <= 0)
return new Matrix<bool>(input.Size);
else if (count >= outOf)
return new Matrix<bool>(input.Size).Fill(true);
var sorted = (int[])input.Data.Clone();
Array.Sort(sorted);
var threshold = sorted[(long)sorted.Length * (outOf - count) / outOf];
return input.Map(v => v >= threshold);
}
/// <summary>
/// For true cells, gives the Chebyshev distance to the closest false cell.
/// For false cells, gives the Chebyshev distance to the closest true cell as a negative.
@@ -785,13 +1011,16 @@ namespace OpenRA.Mods.Common.MapGenerator
/// <para>
/// Given a set of grid-intersection point arrays, creates a matrix where each cell
/// identifies whether the closest points are wrapping around it clockwise or
/// counter-clockwise (as defined in MapUtils.Direction).
/// counter-clockwise (as defined in MapGenerator.Direction).
/// </para>
/// <para>
/// Positive output values indicate the points are wrapping around it clockwise.
/// Negative output values indicate the points are wrapping around it counter-clockwise.
/// Outputs can be zero or non-unit magnitude if there are fighting point arrays.
/// </para>
/// <para>
/// If no points are on or close enough to the matrix area, returns null.
/// </para>
/// </summary>
public static Matrix<int> PointsChirality(int2 size, IEnumerable<int2[]> pointArrayArray)
{
@@ -809,6 +1038,7 @@ namespace OpenRA.Mods.Common.MapGenerator
}
foreach (var pointArray in pointArrayArray)
{
for (var i = 1; i < pointArray.Length; i++)
{
var from = pointArray[i - 1];
@@ -838,6 +1068,10 @@ namespace OpenRA.Mods.Common.MapGenerator
throw new ArgumentException("Unsupported direction for chirality");
}
}
}
if (seeds.Count == 0)
return null;
int? FillChirality(int2 point, int prop)
{

View File

@@ -538,22 +538,24 @@ namespace OpenRA.Mods.Common.MapGenerator
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, random);
else
if (this.actorPlans.Count == 0 && tiles.Count == 0)
throw new ArgumentException("Cannot paint: no tiles or actors");
PaintTiles(map, paintAt, random);
PaintActors(map, actorPlans, paintAt);
break;
case Replaceability.Tile:
if (tiles.Count == 0)
throw new ArgumentException("Cannot paint: no tiles");
PaintTiles(map, paintAt, random);
PaintActors(map, actorPlans, paintAt);
break;
case Replaceability.Actor:
if (this.actorPlans.Count == 0)
throw new ArgumentException("Cannot paint: no actors");
PaintActors(map, actorPlans, paintAt);
break;
}

View File

@@ -20,9 +20,24 @@ namespace OpenRA.Mods.Common.MapGenerator
const int Scale = 1024;
const int ScaledSqrt2 = 1448;
/// <summary>Amplitude is the same for all wavelengths.</summary>
public static int WhiteAmplitude(int wavelength) => 1;
/// <summary>Amplitude proportional to wavelength.</summary>
public static int PinkAmplitude(int wavelength) => wavelength;
/// <summary>
/// <code>amplitude = wavelength ** (1 / (2 ** clumpiness))</code>
/// Setting clumpiness to 0 is equivalent to pink noise.
/// </summary>
public static int ClumpinessAmplitude(int wavelength, int clumpiness)
{
var amplitude = wavelength;
for (var i = 0; i < clumpiness; i++)
amplitude = Exts.ISqrt(amplitude);
return amplitude;
}
/// <summary>
/// <para>
/// Create noise by combining multiple layers of Perlin noise of halving wavelengths.

View File

@@ -1,147 +0,0 @@
#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 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,
[(start, true)],
Filler,
DirectionExts.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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -241,6 +241,53 @@ namespace OpenRA.Mods.Common.MapGenerator
Brushes = permittedTemplates;
}
/// <summary>
/// Convenience method to create TilingPaths using common settings.
/// Start and end terminal types are the same.
/// PermittedSegments are derived from brushes and inner/terminal types.
/// Loops will automatically use only the inner type.
/// Uses automatic end deviation and loop optimization.
/// </summary>
public static TilingPath QuickCreate(
Map map,
IReadOnlyList<MultiBrush> brushes,
CPos[] points,
int maxDeviation,
string innerSegmentType,
string terminalSegmentType)
{
var nonLoopedRoadPermittedTemplates =
PermittedSegments.FromInnerAndTerminalTypes(
brushes, [innerSegmentType], [terminalSegmentType]);
var loopedRoadPermittedTemplates =
PermittedSegments.FromType(brushes, [innerSegmentType]);
var isLoop = points[0] == points[^1];
TilingPath path;
if (isLoop)
path = new TilingPath(
map,
points,
maxDeviation,
innerSegmentType,
innerSegmentType,
loopedRoadPermittedTemplates);
else
path = new TilingPath(
map,
points,
maxDeviation,
terminalSegmentType,
terminalSegmentType,
nonLoopedRoadPermittedTemplates);
path
.SetAutoEndDeviation()
.OptimizeLoop();
return path;
}
sealed class TilingSegment
{
public readonly MultiBrush MultiBrush;
@@ -1360,6 +1407,46 @@ namespace OpenRA.Mods.Common.MapGenerator
return true;
}
/// <summary>Applies StraightenEndsPathPoints to this TilingPath, returning this.</summary>
public TilingPath StraightenEnds(
int shrink,
int grow,
int minimumLength,
int growthInertialRange)
{
Points = StraightenEndsPathPoints(
Points,
CellLayerUtils.CellBounds(Map),
shrink,
grow,
minimumLength,
growthInertialRange);
return this;
}
/// <summary>
/// Straighten the start and end of a path by shrinking and regrowing a straight section.
/// </summary>
/// <param name="points">Points of the path.</param>
/// <param name="bounds">Map bounds, used to identify paths touching edges.</param>
/// <param name="shrink">Distance to shrink path ends (before regrowing them).</param>
/// <param name="grow">Distance to regrow path ends with straightening (after shrinking).</param>
/// <param name="minimumLength">The minimum length (after shrinking, before growth) that paths may be.</param>
/// <param name="growthInertialRange">How many points are used to decide the regrowth direction.</param>
public static CPos[] StraightenEndsPathPoints(
CPos[] points,
Rectangle bounds,
int shrink,
int grow,
int minimumLength,
int growthInertialRange)
{
points = ExtendEdgePathPoints(points, bounds, 2 * shrink + minimumLength);
points = ShrinkPathPoints(points, shrink, minimumLength);
points = InertiallyExtendPathPoints(points, grow, growthInertialRange);
return points;
}
/// <summary>Set MaxEndDeviation.</summary>
public TilingPath SetMaxEndDeviation(int maxEndDeviation)
{

View File

@@ -9,11 +9,9 @@
*/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Mods.Common.MapGenerator;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Support;
using OpenRA.Traits;
@@ -76,43 +74,21 @@ namespace OpenRA.Mods.Common.Traits
var random = new MersenneTwister();
var terrainInfo = modData.DefaultTerrainInfo[args.Tileset];
var map = new Map(modData, terrainInfo, args.Size);
var maxTerrainHeight = map.Grid.MaximumTerrainHeight;
var tl = new PPos(1, 1 + maxTerrainHeight);
var br = new PPos(args.Size.Width - 1, args.Size.Height + maxTerrainHeight - 1);
map.SetBounds(tl, br);
if (!Exts.TryParseUshortInvariant(args.Settings.NodeWithKey("Tile").Value.Value, out var tileType))
throw new YamlException("Illegal tile type");
var tile = new TerrainTile(tileType, 0);
if (!terrainInfo.TryGetTerrainInfo(tile, out var _))
if (!terrainInfo.TryGetTerrainInfo(new TerrainTile(tileType, 0), 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;
}
var map = new Map(modData, terrainInfo, args.Size);
var terraformer = new Terraformer(args, map, modData, [], Symmetry.Mirror.None, 1);
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;
}
terraformer.InitMap();
map.PlayerDefinitions = new MapPlayers(map.Rules, 0).ToMiniYaml();
map.ActorDefinitions = [];
foreach (var mpos in map.AllCells.MapCoords)
map.Tiles[mpos] = terraformer.PickTile(random, tileType);
terraformer.BakeMap();
return map;
}

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
MaximumAltitude: 8
RoughnessRadius: 5
Roughness: 500
MinimumTerrainContourSpacing: 6
MinimumTerrainContourSpacing: 5
MinimumCliffLength: 10
ForestClumpiness: 1
DenyWalledAreas: True
@@ -60,11 +60,8 @@
splitblue: BlueTiberium
ClearTerrain: Clear
PlayableTerrain: Beach,BlueTiberium,Bridge,Clear,Road,Rough,Tiberium,Wall
PartiallyPlayableTerrain: River,Tree,Water
UnplayableTerrain: Rock
DominantTerrain: River,Rock,Tree,Water
ZoneableTerrain: Clear,Road
PartiallyPlayableCategories: Beach,Road
ClearSegmentTypes: Clear
BeachSegmentTypes: Beach
CliffSegmentTypes: Cliff
@@ -158,16 +155,14 @@
Label: label-cnc-map-generator-choice-terrain-type-mountains
Settings:
Water: 0
Mountains: 1000
Mountains: 900
Roughness: 600
MinimumTerrainContourSpacing: 5
Choice@MountainLakes:
Label: label-cnc-map-generator-choice-terrain-type-mountain-lakes
Settings:
Water: 200
Mountains: 1000
Mountains: 900
Roughness: 850
MinimumTerrainContourSpacing: 5
MultiChoiceOption@Shape:
Label: label-cnc-map-generator-option-shape
Default: Square

View File

@@ -23,7 +23,7 @@
MaximumAltitude: 8
RoughnessRadius: 5
Roughness: 500
MinimumTerrainContourSpacing: 6
MinimumTerrainContourSpacing: 5
MinimumCliffLength: 10
ForestClumpiness: 1
DenyWalledAreas: True
@@ -59,11 +59,8 @@
gmine: Gems
ClearTerrain: Clear
PlayableTerrain: Beach,Bridge,Clear,Gems,Ore,Road,Rough,Wall,Water
PartiallyPlayableTerrain: Tree
UnplayableTerrain: River,Rock
DominantTerrain: River,Rock,Tree,Water
ZoneableTerrain: Clear,Road
PartiallyPlayableCategories: Beach,Road
ClearSegmentTypes: Clear
BeachSegmentTypes: Beach
CliffSegmentTypes: Cliff
@@ -156,16 +153,14 @@
Label: label-ra-map-generator-choice-terrain-type-mountains
Settings:
Water: 0
Mountains: 1000
Mountains: 900
Roughness: 600
MinimumTerrainContourSpacing: 5
Choice@MountainLakes:
Label: label-ra-map-generator-choice-terrain-type-mountain-lakes
Settings:
Water: 200
Mountains: 1000
Mountains: 900
Roughness: 850
MinimumTerrainContourSpacing: 5
Choice@Oceanic:
Label: label-ra-map-generator-choice-terrain-type-oceanic
Settings: