diff --git a/OpenRA.Mods.Common/MapGenerator/Matrix.cs b/OpenRA.Mods.Common/MapGenerator/Matrix.cs
index f1bd13640d..168417cd3e 100644
--- a/OpenRA.Mods.Common/MapGenerator/Matrix.cs
+++ b/OpenRA.Mods.Common/MapGenerator/Matrix.cs
@@ -105,6 +105,16 @@ namespace OpenRA.Mods.Common.MapGenerator
return x >= 0 && x < Size.X && y >= 0 && y < Size.Y;
}
+ public bool IsEdge(int x, int y)
+ {
+ return x == 0 || x == Size.X - 1 || y == 0 || y == Size.Y - 1;
+ }
+
+ public bool IsEdge(int2 xy)
+ {
+ return IsEdge(xy.X, xy.Y);
+ }
+
/// Clamp xy to be the closest index within the matrix.
public int2 ClampXY(int2 xy)
{
diff --git a/OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs b/OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs
index dea3f3e029..16315dfad9 100644
--- a/OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs
+++ b/OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs
@@ -1203,6 +1203,30 @@ namespace OpenRA.Mods.Common.MapGenerator
return changes;
}
+ /// Remove links from a direction map that are not reciprocated.
+ public static void RemoveStubsFromDirectionMapInPlace(Matrix matrix)
+ {
+ var output = matrix.Clone();
+ for (var cy = 0; cy < matrix.Size.Y; cy++)
+ for (var cx = 0; cx < matrix.Size.X; cx++)
+ {
+ var fromPos = new int2(cx, cy);
+ var fromDm = matrix[fromPos];
+ foreach (var (offset, d) in Direction.Spread8D)
+ {
+ if ((fromDm & (1 << d)) == 0)
+ continue;
+
+ var dr = Direction.Reverse(d);
+ var toPos = new int2(cx + offset.X, cy + offset.Y);
+ if (matrix.ContainsXY(toPos) && (matrix[toPos] & (1 << dr)) != 0)
+ continue;
+
+ matrix[fromPos] = (byte)(output[fromPos] & ~(1 << d));
+ }
+ }
+ }
+
static Matrix RemoveJunctionsFromDirectionMap(Matrix input)
{
var output = input.Clone();
@@ -1224,67 +1248,116 @@ namespace OpenRA.Mods.Common.MapGenerator
}
}
- for (var x = 0; x < input.Size.X; x++)
- {
- output[x, 0] = (byte)(output[x, 0] & ~(Direction.MLU | Direction.MU | Direction.MRU));
- output[x, input.Size.Y - 1] = (byte)(output[x, input.Size.Y - 1] & ~(Direction.MRD | Direction.MD | Direction.MLD));
- }
-
- for (var y = 0; y < input.Size.Y; y++)
- {
- output[0, y] = (byte)(output[0, y] & ~(Direction.MLD | Direction.ML | Direction.MLU));
- output[input.Size.X - 1, y] &= (byte)(output[input.Size.X - 1, y] & ~(Direction.MRU | Direction.MR | Direction.MRD));
- }
-
return output;
}
///
- /// Traces a matrix of directions into a set of point sequences.
- /// Any junctions in the input direction map are dropped.
+ /// Traces a matrix of directions into a set of point sequences. Each point sequence is
+ /// traced up to but excluding junction points. Paths are traced in both directions. The
+ /// paths in the direction map must be bidrectional and contain no stubs.
///
public static int2[][] DirectionMapToPaths(Matrix input)
{
- input = RemoveJunctionsFromDirectionMap(input);
+ var links = RemoveJunctionsFromDirectionMap(input);
- // Loops not handled, but these would be extremely rare anyway.
+ // Find non-loops, starting at terminals.
var pointArrays = new List();
- for (var sy = 0; sy < input.Size.Y; sy++)
- for (var sx = 0; sx < input.Size.X; sx++)
+
+ void TracePoints(int2 xy, int reverseDm)
+ {
+ var points = new List();
+
+ bool AddPoint()
{
- var sdm = input[sx, sy];
- if (Direction.FromMask(sdm) != Direction.None)
- {
- var points = new List();
- var xy = new int2(sx, sy);
- var reverseDm = 0;
-
- bool AddPoint()
+ points.Add(xy);
+ var dm = links[xy] & ~reverseDm;
+ links[xy] = 0;
+ foreach (var (offset, d) in Direction.Spread8D)
+ if ((dm & (1 << d)) != 0)
{
- points.Add(xy);
- var dm = input[xy] & ~reverseDm;
- foreach (var (offset, d) in Direction.Spread8D)
- if ((dm & (1 << d)) != 0)
- {
- xy += offset;
- if (!input.ContainsXY(xy))
- throw new ArgumentException("input should not link out of bounds");
- reverseDm = 1 << Direction.Reverse(d);
- return true;
- }
-
- return false;
+ xy += offset;
+ reverseDm = 1 << Direction.Reverse(d);
+ return true;
}
- while (AddPoint()) { }
-
- pointArrays.Add(points.ToArray());
- }
+ return false;
}
+ while (AddPoint()) { }
+
+ pointArrays.Add(points.ToArray());
+ pointArrays.Add(points.Reverse().ToArray());
+ }
+
+ for (var sy = 0; sy < links.Size.Y; sy++)
+ for (var sx = 0; sx < links.Size.X; sx++)
+ if (Direction.FromMask(links[sx, sy]) != Direction.None)
+ TracePoints(new int2(sx, sy), 0);
+
+ // All non-loops have been removed, leaving only loops left.
+ for (var sy = 0; sy < links.Size.Y; sy++)
+ for (var sx = 0; sx < links.Size.X; sx++)
+ if (links[sx, sy] != 0)
+ {
+ // Choose direction with most-significant bit
+ var reverseDm = links[sx, sy] & (links[sx, sy] - 1);
+ TracePoints(new int2(sx, sy), reverseDm);
+ }
+
return pointArrays.ToArray();
}
+ ///
+ /// Wrapper around DirectionMapToPaths which iteratively prunes stubs and short paths until
+ /// all paths are at least a minimumLength. Paths shorter than mimimumJunctionSeparation
+ /// sever their neighboring junctions instead of fusing them together.
+ ///
+ public static int2[][] DirectionMapToPathsWithPruning(
+ Matrix input,
+ int minimumLength,
+ int minimumJunctionSeparation,
+ bool preserveEdgePaths)
+ {
+ var links = input.Clone();
+ int2[][] pointArrays;
+
+ // Iteratively remove paths which are too short and merge remaining ones.
+ while (true)
+ {
+ RemoveStubsFromDirectionMapInPlace(links);
+ pointArrays = DirectionMapToPaths(links);
+
+ var removeablePointArrays = pointArrays;
+ if (preserveEdgePaths)
+ removeablePointArrays = pointArrays
+ .Where(a => !(links.IsEdge(a[0]) || links.IsEdge(a[^1])))
+ .ToArray();
+
+ if (removeablePointArrays.Length == 0)
+ return pointArrays;
+
+ var shortest = removeablePointArrays.Min(a => a.Length);
+
+ if (shortest >= minimumLength)
+ return pointArrays;
+
+ var toDelete = new List();
+ foreach (var pointArray in removeablePointArrays)
+ if (pointArray.Length == shortest)
+ foreach (var point in pointArray)
+ {
+ toDelete.Add(point);
+ if (pointArray.Length < minimumJunctionSeparation)
+ foreach (var (offset, d) in Direction.Spread8D)
+ if ((links[point] & (1 << d)) != 0)
+ toDelete.Add(point + offset);
+ }
+
+ foreach (var point in toDelete)
+ links[point] = 0;
+ }
+ }
+
///
///
/// Given a set of point sequences and a stencil mask that defines permitted point positions,
diff --git a/OpenRA.Mods.Common/MapGenerator/TilingPath.cs b/OpenRA.Mods.Common/MapGenerator/TilingPath.cs
index 4658ee1546..153d596280 100644
--- a/OpenRA.Mods.Common/MapGenerator/TilingPath.cs
+++ b/OpenRA.Mods.Common/MapGenerator/TilingPath.cs
@@ -395,6 +395,9 @@ namespace OpenRA.Mods.Common.MapGenerator
? forwardProgressLimit
: int.MinValue;
+ // Find the progress difference of two progress values. For loops high progress values
+ // wrap around to low ones. (Think of loops' progress like a 24 hour clock,
+ // where 22 -> 2 is a difference of 4, and 2 -> 22 is a difference of -4).
int Progress(int from, int to)
{
if (IsLoop)
@@ -425,11 +428,24 @@ namespace OpenRA.Mods.Common.MapGenerator
(int Low, int High) FindLowAndHigh(List values)
{
- Debug.Assert(values.Count > 0, "No values");
+ if (values.Count == 0)
+ return (InvalidProgress, InvalidProgress);
+
if (values.Count == 1)
return (values[0], values[0]);
+
if (IsLoop)
{
+ // For loops, with a list of 2+ sorted progress values, there are 2 cases:
+ // - The values are spatially grouped, such that the values are contained
+ // in under a half of the progress range, and the largest gap between
+ // values is more than half of the progress range. This means that going
+ // from before the gap to after it is an overall negative progress
+ // change. (It must be the only negative progress change one as there can
+ // only be one gap that is over half of the progress range.) In this
+ // case, there is an obvious start and end to the group, with an overall
+ // positive progress change.
+ // - The values are dispersed such that there is no obvious start or end.
if (Progress(values[^1], values[0]) < 0)
return (values[0], values[^1]);
for (var i = 0; i < values.Count - 1; i++)
@@ -459,9 +475,8 @@ namespace OpenRA.Mods.Common.MapGenerator
lows.Clear();
highs.Clear();
- for (var i = 0; i < 8; i++)
+ foreach (var offset in Direction.Spread8)
{
- var offset = Direction.Spread8[i];
var neighbor = xy + offset;
if (!deviations.ContainsXY(neighbor) ||
deviations[neighbor] >= deviation ||
@@ -839,12 +854,19 @@ namespace OpenRA.Mods.Common.MapGenerator
///
/// Extend the start and end of a path by extensionLength points. The directions of the
/// extensions are based on the overall direction of the outermost inertialRange points.
+ /// Loops are left unmodified.
///
public static CPos[] InertiallyExtendPathPoints(CPos[] points, int extensionLength, int inertialRange)
{
if (points == null)
return null;
+ if (points[0] == points[^1])
+ {
+ // Is a loop.
+ return points;
+ }
+
if (inertialRange > points.Length - 1)
inertialRange = points.Length - 1;
var sd = Direction.FromCVecNonDiagonal(points[inertialRange] - points[0]);
@@ -1107,6 +1129,9 @@ namespace OpenRA.Mods.Common.MapGenerator
/// Normalized but opposing paths rotate around the center in the same direction.
///
///
+ /// Loops are normalized to rotate in a consistent direction, regardless of position.
+ ///
+ ///
/// The measureFromCenter function must convert CVec positions to WVec offsets from the map
/// center.
///
@@ -1122,34 +1147,56 @@ namespace OpenRA.Mods.Common.MapGenerator
if (start == end)
{
- // Is loop
- start = points[1];
- end = points[^2];
- }
+ // Is a loop.
+ // Find the top-left-most corner point (on the convex hull) and
+ // sample which way the points are bending.
+ var topLeftIndex = 0;
+ var topLeftPoint = points[0];
+ for (var i = 1; i < points.Length; i++)
+ {
+ var point = points[i];
+ if (point.Y < topLeftPoint.Y || (point.Y == topLeftPoint.Y && point.X < topLeftPoint.X))
+ {
+ topLeftIndex = i;
+ topLeftPoint = point;
+ }
+ }
- bool ShouldReverse(CPos start, CPos end)
+ var inOffset = points[topLeftIndex] - points[(topLeftIndex + points.Length - 1) % points.Length];
+ var outOffset = points[(topLeftIndex + points.Length + 1) % points.Length] - points[topLeftIndex];
+ var crossProd = inOffset.X * outOffset.Y - inOffset.Y * outOffset.X;
+
+ // crossProd should never be 0 for a valid input.
+ if (crossProd < 0)
+ Array.Reverse(normalized);
+ }
+ else
{
- var v1 = measureFromCenter(start);
- var v2 = measureFromCenter(end);
+ // Is not a loop.
+ bool ShouldReverse(CPos start, CPos end)
+ {
+ var v1 = measureFromCenter(start);
+ var v2 = measureFromCenter(end);
- // Rotation around center?
- var crossProd = v1.X * v2.Y - v2.X * v1.Y;
- if (crossProd != 0)
- return crossProd < 0;
+ // Rotation around center?
+ var crossProd = v1.X * v2.Y - v2.X * v1.Y;
+ if (crossProd != 0)
+ return crossProd < 0;
- // Distance from center?
- var r1 = v1.X * v1.X + v1.Y * v1.Y;
- var r2 = v2.X * v2.X + v2.Y * v2.Y;
- if (r1 != r2)
- return r1 < r2;
+ // Distance from center?
+ var r1 = v1.X * v1.X + v1.Y * v1.Y;
+ var r2 = v2.X * v2.X + v2.Y * v2.Y;
+ if (r1 != r2)
+ return r1 < r2;
- // Absolute angle
- return v1.Y == v2.Y ? v1.X > v2.X : v1.Y > v2.Y;
+ // Absolute angle
+ return v1.Y == v2.Y ? v1.X > v2.X : v1.Y > v2.Y;
+ }
+
+ if (ShouldReverse(start, end))
+ Array.Reverse(normalized);
}
- if (ShouldReverse(start, end))
- Array.Reverse(normalized);
-
return normalized;
}
@@ -1191,5 +1238,42 @@ namespace OpenRA.Mods.Common.MapGenerator
return outputs.ToArray();
}
+
+ /// Nullify the path's points if they aren't suitable for tiling.
+ public TilingPath RetainIfValid()
+ {
+ if (!ValidatePathPoints(Points))
+ Points = null;
+
+ return this;
+ }
+
+ public static bool ValidatePathPoints(CPos[] points)
+ {
+ if (points == null || points.Length == 0)
+ return false;
+
+ var isLoop = points[0] == points[^1];
+
+ if (points.Length < (isLoop ? 3 : 2))
+ return false;
+
+ // Duplicate points check
+ if (points.Distinct().Count() != points.Length - (isLoop ? 1 : 0))
+ return false;
+
+ // All steps must be (non-diagonal) unit offsets.
+ var lastPoint = points[0];
+ for (var i = 1; i < points.Length; i++)
+ {
+ var offset = lastPoint - points[i];
+ if (Direction.ToCVec(Direction.FromCVecNonDiagonal(offset)) != offset)
+ return false;
+
+ lastPoint = points[i];
+ }
+
+ return true;
+ }
}
}
diff --git a/OpenRA.Mods.Common/Traits/World/RaMapGenerator.cs b/OpenRA.Mods.Common/Traits/World/RaMapGenerator.cs
index f9d237e82f..4b47af9d6e 100644
--- a/OpenRA.Mods.Common/Traits/World/RaMapGenerator.cs
+++ b/OpenRA.Mods.Common/Traits/World/RaMapGenerator.cs
@@ -1037,7 +1037,12 @@ namespace OpenRA.Mods.Common.Traits
{
var space = new CellLayer(map);
foreach (var mpos in map.AllCells.MapCoords)
- space[mpos] = playableArea[mpos] && param.ClearTerrain.Contains(tileset.GetTerrainIndex(map.Tiles[mpos]));
+ space[mpos] = param.ClearTerrain.Contains(tileset.GetTerrainIndex(map.Tiles[mpos]));
+
+ foreach (var actorPlan in actorPlans)
+ foreach (var (cpos, _) in actorPlan.Footprint())
+ if (space.Contains(cpos))
+ space[cpos] = false;
// Improve symmetry.
{
@@ -1052,7 +1057,14 @@ namespace OpenRA.Mods.Common.Traits
space = newSpace;
}
- var matrixSpace = CellLayerUtils.ToMatrix(space, false);
+ // TODO: Move to configuration
+ const int RoadStraightenShrink = 4;
+ const int RoadStraightenGrow = 2;
+ const int RoadMinimumShrinkLength = 12;
+ const int RoadInertialRange = 8;
+ var roadTotalShrink = RoadStraightenShrink + param.RoadShrink;
+
+ var matrixSpace = CellLayerUtils.ToMatrix(space, true);
var kernel = new Matrix(param.RoadSpacing * 2 + 1, param.RoadSpacing * 2 + 1);
MatrixUtils.OverCircle(
matrix: kernel,
@@ -1065,30 +1077,50 @@ namespace OpenRA.Mods.Common.Traits
kernel,
new int2(param.RoadSpacing, param.RoadSpacing),
false);
- var deflated = MatrixUtils.DeflateSpace(dilated, false);
- var matrixPointArrays = MatrixUtils.DirectionMapToPaths(deflated);
+ var deflated = MatrixUtils.DeflateSpace(dilated, true);
+ var matrixPointArrays = MatrixUtils.DirectionMapToPathsWithPruning(
+ input: deflated,
+ minimumLength: 20 + 2 * param.RoadShrink,
+ minimumJunctionSeparation: 6,
+ preserveEdgePaths: true);
var pointArrays = CellLayerUtils.FromMatrixPoints(matrixPointArrays, space);
pointArrays = TilingPath.RetainDisjointPaths(pointArrays);
- var roadPermittedTemplates =
+ var nonLoopedRoadPermittedTemplates =
TilingPath.PermittedSegments.FromInnerAndTerminalTypes(
tileset, param.RoadSegmentTypes, param.ClearSegmentTypes);
+ var loopedRoadPermittedTemplates =
+ TilingPath.PermittedSegments.FromType(
+ tileset, param.RoadSegmentTypes);
foreach (var pointArray in pointArrays)
{
- // Currently, never looped.
- var path = new TilingPath(
- map,
- pointArray,
- param.RoadSpacing - 1,
- param.ClearSegmentTypes[0],
- param.ClearSegmentTypes[0],
- roadPermittedTemplates);
+ var isLoop = pointArray[0] == pointArray[^1];
+ TilingPath path;
+ if (isLoop)
+ path = new TilingPath(
+ map,
+ pointArray,
+ param.RoadSpacing - 1,
+ param.RoadSegmentTypes[0],
+ param.RoadSegmentTypes[0],
+ loopedRoadPermittedTemplates);
+ else
+ path = new TilingPath(
+ map,
+ pointArray,
+ param.RoadSpacing - 1,
+ param.ClearSegmentTypes[0],
+ param.ClearSegmentTypes[0],
+ nonLoopedRoadPermittedTemplates);
+
path
.ChirallyNormalize(cvec => CellLayerUtils.CornerToWPos(cvec, gridType) - wMapCenter)
- .Shrink(4 + param.RoadShrink, 12)
- .InertiallyExtend(2, 8)
- .ExtendEdge(4);
+ .ExtendEdge(2 * roadTotalShrink + RoadMinimumShrinkLength)
+ .Shrink(roadTotalShrink, RoadMinimumShrinkLength)
+ .InertiallyExtend(RoadStraightenGrow, RoadInertialRange)
+ .OptimizeLoop()
+ .RetainIfValid();
// Shrinking may have deleted the path.
if (path.Points == null)