Improve map-generated roads for awkward symmetries

For awkward symmetries (where there is mirroring and/or non-grid
aligned rotations), road planning now runs an extra filtering step so
that only sufficiently symmetric road plans make it to the tiling
stage.

Road planning also now extends out-of-map-bounds to more accurately
accommodate awkward rotations, improving the symmetry of pre-filtered
road plans.

A previous filter, which removes nearly horizontal or vertical roads
is now removed, as this is not fair to all rotations. This step is
less importance since the introduction of relaxed tiling.

Note that roads may be dropped entirely if the plans come out highly
asymmetric. This is not uncommon for awkward rotations. This is
preferred over presenting a map with asymmetric roads.
This commit is contained in:
Ashley Newson
2025-04-22 20:08:11 +01:00
committed by Gustas Kažukauskas
parent c541a0e0dc
commit 07ef98a578

View File

@@ -1086,18 +1086,38 @@ namespace OpenRA.Mods.Common.Traits
if (param.Roads)
{
var space = new CellLayer<bool>(map);
foreach (var mpos in map.AllCells.MapCoords)
space[mpos] = param.ClearTerrain.Contains(tileset.GetTerrainIndex(map.Tiles[mpos]));
// For awkward symmetries, we try harder to make sure roads are fairer.
// This can degrade the quantity of roads, though.
var imperfectSymmetry =
param.Mirror != Symmetry.Mirror.None ||
param.Rotations == 3 ||
param.Rotations >= 5;
// Enlargement must increase dimensions by multiple of 4 to maximize compatibility
// with IsometricRectangular grids, where a non-multiple of 4 would change how the
// center aligns with the grid.
var enlargedSize = new Size(
map.MapSize.Width + (map.MapSize.Width & ~3) + 4,
map.MapSize.Height + (map.MapSize.Height & ~3) + 4);
var space = new CellLayer<bool>(gridType, enlargedSize);
space.Clear(true);
var enlargedOffset =
CellLayerUtils.WPosToCPos(CellLayerUtils.Center(space), gridType)
- CellLayerUtils.WPosToCPos(CellLayerUtils.Center(map.Tiles), gridType);
foreach (var cpos in map.AllCells)
space[cpos + enlargedOffset] = param.ClearTerrain.Contains(tileset.GetTerrainIndex(map.Tiles[cpos]));
foreach (var actorPlan in actorPlans)
foreach (var (cpos, _) in actorPlan.Footprint())
if (space.Contains(cpos))
space[cpos] = false;
if (space.Contains(cpos + enlargedOffset))
space[cpos + enlargedOffset] = false;
// Improve symmetry.
{
var newSpace = new CellLayer<bool>(map);
var newSpace = new CellLayer<bool>(gridType, enlargedSize);
Symmetry.RotateAndMirrorOverCPos(
space,
param.Rotations,
@@ -1114,6 +1134,7 @@ namespace OpenRA.Mods.Common.Traits
const int RoadMinimumShrinkLength = 12;
const int RoadInertialRange = 8;
var roadTotalShrink = RoadStraightenShrink + param.RoadShrink;
var minimumRoadLengthForPruning = RoadMinimumShrinkLength + 2 * roadTotalShrink;
var matrixSpace = CellLayerUtils.ToMatrix(space, true);
var kernel = new Matrix<bool>(param.RoadSpacing * 2 + 1, param.RoadSpacing * 2 + 1);
@@ -1129,13 +1150,70 @@ namespace OpenRA.Mods.Common.Traits
new int2(param.RoadSpacing, param.RoadSpacing),
false);
var deflated = MatrixUtils.DeflateSpace(dilated, true);
if (imperfectSymmetry)
{
var changing = true;
while (changing)
{
changing = false;
// Delete short paths.
{
MatrixUtils.RemoveStubsFromDirectionMapInPlace(deflated);
var paths = MatrixUtils.DirectionMapToPaths(deflated);
if (paths.Length == 0)
break;
var minLength = paths.Min(p => p.Length);
if (minLength < minimumRoadLengthForPruning)
{
changing = true;
var shortPaths = paths
.Where(path => path.Length == minLength);
foreach (var path in shortPaths)
foreach (var point in path)
deflated[point] = 0;
MatrixUtils.RemoveStubsFromDirectionMapInPlace(deflated);
}
}
// Prune asymmetric paths.
{
const int Dilation = 3;
var nearPath = MatrixUtils.KernelDilateOrErode(
deflated.Map(v => v != 0),
new Matrix<bool>(Dilation * 2 + 1, Dilation * 2 + 1).Fill(true),
new int2(Dilation, Dilation),
true);
var matrixPaths = MatrixUtils.DirectionMapToPaths(deflated);
foreach (var path in matrixPaths)
{
var cposPath = CellLayerUtils.FromMatrixPoints([path], space)[0];
var projectedPoints = cposPath
.SelectMany(p => Symmetry.RotateAndMirrorCPos(p, space, param.Rotations, param.Mirror))
.ToArray();
var matrixPoints = CellLayerUtils.ToMatrixPoints([projectedPoints], space)[0];
if (!matrixPoints.All(p => !nearPath.ContainsXY(p) || nearPath[p]))
{
// The path doesn't exist across all symmetries (or isn't consistent enough).
changing = true;
foreach (var point in path)
deflated[point] = 0;
}
}
}
}
}
var matrixPointArrays = MatrixUtils.DirectionMapToPathsWithPruning(
input: deflated,
minimumLength: 20 + 2 * param.RoadShrink,
minimumLength: minimumRoadLengthForPruning,
minimumJunctionSeparation: 6,
preserveEdgePaths: true);
var pointArrays = CellLayerUtils.FromMatrixPoints(matrixPointArrays, space);
pointArrays = TilingPath.RetainDisjointPaths(pointArrays);
pointArrays = pointArrays.Select(a => a.Select(p => p - enlargedOffset).ToArray()).ToArray();
var nonLoopedRoadPermittedTemplates =
TilingPath.PermittedSegments.FromInnerAndTerminalTypes(
@@ -1178,14 +1256,6 @@ namespace OpenRA.Mods.Common.Traits
if (path.Points == null)
continue;
// Roads that are _almost_ vertical or horizontal tile badly. Filter them out.
var minX = path.Points.Min((p) => p.X);
var minY = path.Points.Min((p) => p.Y);
var maxX = path.Points.Max((p) => p.X);
var maxY = path.Points.Max((p) => p.Y);
if (maxX - minX < 6 || maxY - minY < 6)
continue;
var brush = path.Tile(roadTilingRandom)
?? throw new MapGenerationException("Could not fit tiles for roads");
brush.Paint(map, actorPlans, CPos.Zero, MultiBrush.Replaceability.Tile);