Support relaxed end points in TilingPath

Whilst TilingPath has always allowed deviating from the path points
between the start and end point, it didn't allow deviation from the
start or end points themselves. This change allows the end point to
deviate if the tiling would otherwise fail. The start point remains
strictly positioned, as before. Loop end points (which must necessarily
match the start) also remain strict.

The ExperimentalMapGenerator is modified to benefit from the relaxed
tiling constraints. As a result, failed map generation due to tiling
failures is much rarer, and will make some otherwise very inflexible
template categories viable for tiling.

Includes some impure refactoring around the treatment of start,
intermediate, and end segments. As such, this changes map generation
output, even for maps which already tiled perfectly.

A previous workaround used for CnC road templates, whereby additional
segments were defined for road ending templates, is now redundant and
cleaned up.
This commit is contained in:
Ashley Newson
2025-04-06 23:24:42 +01:00
committed by Gustas Kažukauskas
parent b2acc653a0
commit f09959ecba
7 changed files with 204 additions and 226 deletions

View File

@@ -15,6 +15,7 @@ using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using OpenRA.Primitives;
using OpenRA.Support;
namespace OpenRA.Mods.Common.MapGenerator
{
@@ -1502,5 +1503,34 @@ namespace OpenRA.Mods.Common.MapGenerator
return matrix;
}
/// <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 (int2 MPos, T Value) FindRandomBest<T>(
Matrix<T> matrix,
MersenneTwister random,
Comparison<T> comparison)
{
var candidates = new List<int2>();
var best = matrix[new int2(0, 0)];
for (var y = 0; y < matrix.Size.Y; y++)
for (var x = 0; x < matrix.Size.X; x++)
{
var rank = comparison(matrix[x, y], best);
if (rank > 0)
{
best = matrix[x, y];
candidates.Clear();
}
if (rank >= 0)
candidates.Add(new int2(x, y));
}
var choice = candidates[random.Next(candidates.Count)];
return (choice, best);
}
}
}

View File

@@ -212,6 +212,13 @@ namespace OpenRA.Mods.Common.MapGenerator
/// </summary>
public int MinSeparation;
/// <summary>
/// If the path cannot be tiled exactly, the resulting tiling is allowed to deviate from
/// target end point by this Chebychev distance. Ignored for loops. This will be capped to
/// MaxDeviation at tiling time.
/// </summary>
public int MaxEndDeviation;
/// <summary>
/// Stores start type and direction.
/// </summary>
@@ -242,6 +249,7 @@ namespace OpenRA.Mods.Common.MapGenerator
MaxDeviation = maxDeviation;
MaxSkip = 0;
MinSeparation = 0;
MaxEndDeviation = 0;
Start = new Terminal(startType, null);
End = new Terminal(endType, null);
Segments = permittedTemplates;
@@ -337,6 +345,9 @@ namespace OpenRA.Mods.Common.MapGenerator
// The search is conducted from the path start node until the best possible cost of
// the end node is confirmed. This also populates possible intermediate nodes' costs.
//
// If the original target end node is unreachable (at MaxCost), a nearby node may be
// selected as a fallback end point, provided the target path isn't a loop.
//
// Then, from the end node, it works backwards. It finds any (random) suitable template
// segment which connects back to a previous node where the difference in cost is
// that of the template segment's cost, implying that the previous node is on an
@@ -563,12 +574,25 @@ namespace OpenRA.Mods.Common.MapGenerator
var pathEnd = points[^1];
var orderedPermittedSegments = Segments.All.ToImmutableArray();
var permittedSegments = orderedPermittedSegments.ToImmutableHashSet();
var permittedStartSegments = Segments.Start.ToImmutableHashSet();
var permittedInnerSegments = Segments.Inner.ToImmutableHashSet();
var permittedEndSegments = Segments.End.ToImmutableHashSet();
const int MaxCost = int.MaxValue;
var segmentTypeToId = new Dictionary<string, int>();
var segmentsByStart = new List<List<TilingSegment>>();
var segmentsByEnd = new List<List<TilingSegment>>();
var costs = new List<Matrix<int>>();
var segmentsByStart = new List<List<(TilingSegment Segment, bool CanStart, bool CanInner, bool CanEnd)>>();
var segmentsByEnd = new List<List<(TilingSegment Segment, bool CanStart, bool CanInner, bool CanEnd)>>();
// We store the end costs of valid end segments separately to inner costs.
//
// Note also that:
// - The start cost is always zero and only applies to a single node.
// - Permitted end and inner segments may be distinct, but the end terminal could exist
// in the permitted inner segments and shouldn't be a valid intermediate cost.
// - Avoids confusing start, inner, and end costs when processing looped paths.
// - We may be interested in multiple end costs if MaxEndDeviation is non-zero.
var endCosts = new Matrix<int>(size).Fill(MaxCost);
var innerCosts = new List<Matrix<int>>();
{
void RegisterSegmentType(string type)
{
@@ -578,7 +602,7 @@ namespace OpenRA.Mods.Common.MapGenerator
segmentTypeToId.Add(type, newId);
segmentsByStart.Add([]);
segmentsByEnd.Add([]);
costs.Add(new Matrix<int>(size).Fill(MaxCost));
innerCosts.Add(new Matrix<int>(size).Fill(MaxCost));
}
foreach (var segment in orderedPermittedSegments)
@@ -589,8 +613,13 @@ namespace OpenRA.Mods.Common.MapGenerator
var startTypeId = segmentTypeToId[segment.Start];
var endTypeId = segmentTypeToId[segment.End];
var tilePathSegment = new TilingSegment(template, segment, startTypeId, endTypeId);
segmentsByStart[startTypeId].Add(tilePathSegment);
segmentsByEnd[endTypeId].Add(tilePathSegment);
var tuple = (
tilePathSegment,
permittedStartSegments.Contains(segment) && segment.Start == start.SegmentType,
permittedInnerSegments.Contains(segment),
permittedEndSegments.Contains(segment) && segment.End == end.SegmentType);
segmentsByStart[startTypeId].Add(tuple);
segmentsByEnd[endTypeId].Add(tuple);
}
}
@@ -610,43 +639,18 @@ namespace OpenRA.Mods.Common.MapGenerator
var pathStartTypeId = segmentTypeToId[start.SegmentType];
var pathEndTypeId = segmentTypeToId[end.SegmentType];
var innerTypeIds = Segments.Inner
.SelectMany(segment => new[] { segment.Start, segment.End })
.Select(segmentType => segmentTypeToId[segmentType])
.ToImmutableHashSet();
// Lower (closer to zero) costs are better matches.
// MaxScore means totally unacceptable.
int ScoreSegment(TilingSegment segment, CVec from)
{
if (from == pathStart)
{
if (segment.StartTypeId != pathStartTypeId)
return MaxCost;
}
else
{
if (!innerTypeIds.Contains(segment.StartTypeId))
return MaxCost;
}
var to = from + segment.Moves;
if (to == pathEnd)
{
if (segment.EndTypeId != pathEndTypeId)
return MaxCost;
}
else
{
if (!innerTypeIds.Contains(segment.EndTypeId))
return MaxCost;
if (isLoop && lowProgress[from.X, from.Y] > highProgress[to.X, to.Y] && highProgress[to.X, to.Y] != 0)
{
// We've missed the start/end of the loop and have potentially gone past it
// (as far as low and high progress are concerned).
return MaxCost;
}
if (isLoop && to != pathEnd && lowProgress[from.X, from.Y] > highProgress[to.X, to.Y] && highProgress[to.X, to.Y] != 0)
{
// We've missed the start/end of the loop and have potentially gone past it
// (as far as low and high progress are concerned).
return MaxCost;
}
var deviationAcc = 0;
@@ -700,10 +704,23 @@ namespace OpenRA.Mods.Common.MapGenerator
return deviationAcc;
}
void UpdateFrom(CVec from, int fromTypeId, int fromCost)
void UpdateFrom(CVec from, int fromTypeId, bool isForStart)
{
foreach (var segment in segmentsByStart[fromTypeId])
var fromCost = isForStart ? 0 : innerCosts[fromTypeId][from.X, from.Y];
foreach (var (segment, canStart, canInner, canEnd) in segmentsByStart[fromTypeId])
{
if (isForStart)
{
if (!canStart)
continue;
}
else
{
if (!(canEnd || canInner))
continue;
}
var to = from + segment.Moves;
if (to.X < 0 || to.X >= size.X || to.Y < 0 || to.Y >= size.Y)
continue;
@@ -721,44 +738,59 @@ namespace OpenRA.Mods.Common.MapGenerator
var toCost = fromCost + segmentCost;
var toTypeId = segment.EndTypeId;
if (toCost < costs[toTypeId][to.X, to.Y])
if ((canStart || canInner) && toCost < innerCosts[toTypeId][to.X, to.Y])
{
costs[toTypeId][to.X, to.Y] = toCost;
innerCosts[toTypeId][to.X, to.Y] = toCost;
SetPriorityAt(toTypeId, to, toCost);
}
if (canEnd && toCost < endCosts[to.X, to.Y])
endCosts[to.X, to.Y] = toCost;
}
SetPriorityAt(fromTypeId, from, MaxCost);
}
// costs[pathStartTypeId][pathStart.X, pathStart.Y] is preset to
// MaxCost, but we pass in a cost of 0 for the first iteration. We
// leave it like this in case this is a looped path with a shared
// start and end point. We set it to 0 later when tracing back.
UpdateFrom(pathStart, pathStartTypeId, 0);
UpdateFrom(pathStart, pathStartTypeId, true);
while (true)
{
var (fromTypeId, from, priority) = GetNextPriority();
if (priority == MaxCost || from == pathEnd)
if (priority == MaxCost)
break;
UpdateFrom(from, fromTypeId, costs[fromTypeId][from.X, from.Y]);
UpdateFrom(from, fromTypeId, false);
}
// Trace back and update tiles
var resultPoints = new List<CPos>
{
new(pathEnd.X + minPoint.X, pathEnd.Y + minPoint.Y)
};
var resultPoints = new List<CPos>();
(CVec From, int FromTypeId) TraceBackStep(CVec to, int toTypeId, int toCost)
(CVec From, int FromTypeId) TraceBackStep(CVec to, int toTypeId, bool isForEnd)
{
var toCost = isForEnd ? endCosts[to.X, to.Y] : innerCosts[toTypeId][to.X, to.Y];
var candidates = new List<TilingSegment>();
foreach (var segment in segmentsByEnd[toTypeId])
foreach (var (segment, canStart, canInner, canEnd) in segmentsByEnd[toTypeId])
{
if (isForEnd)
{
if (!canEnd)
continue;
}
else
{
if (!(canStart || canInner))
continue;
}
var from = to - segment.Moves;
var mustStart =
from == pathStart && segment.StartTypeId == pathStartTypeId;
if (mustStart && !canStart)
continue;
if (from.X < 0 || from.X >= size.X || from.Y < 0 || from.Y >= size.Y)
continue;
@@ -774,7 +806,9 @@ namespace OpenRA.Mods.Common.MapGenerator
continue;
var fromCost = toCost - segmentCost;
if (fromCost == costs[segment.StartTypeId][from.X, from.Y])
var requiredFromCost =
mustStart ? 0 : innerCosts[segment.StartTypeId][from.X, from.Y];
if (fromCost == requiredFromCost)
candidates.Add(segment);
}
@@ -794,24 +828,80 @@ namespace OpenRA.Mods.Common.MapGenerator
}
{
var to = pathEnd;
var toTypeId = pathEndTypeId;
var bestCost = costs[toTypeId][to.X, to.Y];
if (bestCost == MaxCost)
return null;
// For non-loops, this remained unset at MaxCost. For loops,
// this was the shared start and end point and got set to
// bestCost. We set it to 0 for traceback, but perform the
// first iteration using bestCost. (The opposite of how we
// traced forward.)
costs[pathStartTypeId][pathStart.X, pathStart.Y] = 0;
if (endCosts[pathEnd.X, pathEnd.Y] == MaxCost)
{
// There isn't a tiling solution to the exact target end point. If enabled,
// search for an alternative, nearby end point.
var maxEndDeviation = Math.Min(MaxEndDeviation, MaxDeviation);
if (maxEndDeviation == 0 || isLoop)
return null;
(to, toTypeId) = TraceBackStep(to, toTypeId, bestCost);
// Find the closest points which are near the original target end point and
// have a tiling solution.
const int Unreached = int.MaxValue;
const int Unsolved = int.MaxValue - 1;
var fallbackDistances =
new Matrix<int>(maxEndDeviation * 2 + 1, maxEndDeviation * 2 + 1)
.Fill(Unreached);
int? FallbacksFiller(int2 xy, int distance)
{
if (fallbackDistances[xy] != Unreached)
return null;
var p = new int2(pathEnd.X - maxEndDeviation, pathEnd.Y - maxEndDeviation) + xy;
if (!deviations.ContainsXY(p.X, p.Y) || deviations[p.X, p.Y] == OverDeviation)
{
fallbackDistances[xy] = Unsolved;
return null;
}
fallbackDistances[xy] =
endCosts[p.X, p.Y] != MaxCost ? distance : Unsolved;
return distance + 1;
}
MatrixUtils.FloodFill(
fallbackDistances.Size,
[(new int2(maxEndDeviation, maxEndDeviation), 0)],
FallbacksFiller,
Direction.Spread4);
var bestDistance = fallbackDistances.Data.Min();
if (bestDistance == Unreached || bestDistance == Unsolved)
return null;
// Find the lowest cost candidate end point.
var fallbackCosts = new Matrix<int>(maxEndDeviation * 2 + 1, maxEndDeviation * 2 + 1);
for (var y = -maxEndDeviation; y <= maxEndDeviation; y++)
for (var x = -maxEndDeviation; x <= maxEndDeviation; x++)
{
var fallbackXy = new int2(x + maxEndDeviation, y + maxEndDeviation);
var p = new int2(x + pathEnd.X, y + pathEnd.Y);
fallbackCosts[fallbackXy] =
(fallbackDistances[fallbackXy] == bestDistance) ? endCosts[p] : MaxCost;
}
var (chosenXy, _) = MatrixUtils.FindRandomBest(
fallbackCosts,
random,
(a, b) => b.CompareTo(a));
pathEnd = new CVec(chosenXy.X - maxEndDeviation, chosenXy.Y - maxEndDeviation) + pathEnd;
}
var to = pathEnd;
resultPoints.Add(new(to.X + minPoint.X, to.Y + minPoint.Y));
(to, toTypeId) = TraceBackStep(to, toTypeId, true);
// No need to check direction. If that is an issue, I have bigger problems to worry about.
while (to != pathStart)
(to, toTypeId) = TraceBackStep(to, toTypeId, costs[toTypeId][to.X, to.Y]);
(to, toTypeId) = TraceBackStep(to, toTypeId, false);
}
// Traced back in reverse, so reverse the reversal.
@@ -1275,5 +1365,19 @@ namespace OpenRA.Mods.Common.MapGenerator
return true;
}
/// <summary>Set MaxEndDeviation.</summary>
public TilingPath SetMaxEndDeviation(int maxEndDeviation)
{
MaxEndDeviation = maxEndDeviation;
return this;
}
/// <summary>Allow end point deviation as far as MaxDeviation will allow.</summary>
public TilingPath SetAutoEndDeviation()
{
MaxEndDeviation = int.MaxValue;
return this;
}
}
}

View File

@@ -666,6 +666,7 @@ namespace OpenRA.Mods.Common.Traits
beachPermittedTemplates);
beachPath
.ExtendEdge(4)
.SetAutoEndDeviation()
.OptimizeLoop();
tiledBeaches[i] =
beachPath.Tile(beachTilingRandom)
@@ -732,6 +733,7 @@ namespace OpenRA.Mods.Common.Traits
nonLoopedCliffPermittedTemplates);
cliffPath
.ExtendEdge(4)
.SetAutoEndDeviation()
.OptimizeLoop();
if (cliffPath.Tile(cliffTilingRandom) == null)
throw new MapGenerationException("Could not fit tiles for exterior circle cliffs");
@@ -812,6 +814,7 @@ namespace OpenRA.Mods.Common.Traits
nonLoopedCliffPermittedTemplates);
cliffPath
.ExtendEdge(4)
.SetAutoEndDeviation()
.OptimizeLoop();
if (cliffPath.Tile(cliffTilingRandom) == null)
throw new MapGenerationException("Could not fit tiles for cliffs");
@@ -1176,6 +1179,7 @@ namespace OpenRA.Mods.Common.Traits
.ExtendEdge(2 * roadTotalShrink + RoadMinimumShrinkLength)
.Shrink(roadTotalShrink, RoadMinimumShrinkLength)
.InertiallyExtend(RoadStraightenGrow, RoadInertialRange)
.SetAutoEndDeviation()
.OptimizeLoop()
.RetainIfValid();

View File

@@ -1069,16 +1069,6 @@ Templates:
Inner: Road
Points: 2,1, 2,0
Start: Clear.U
Segment@1extended:
End: Clear.D
Inner: Road
Points: 1,0, 1,1, 1,2
Start: Road.D
Segment@2extended:
End: Road.U
Inner: Road
Points: 2,2, 2,1, 2,0
Start: Clear.U
Template@94:
Id: 94
Images: d02.des
@@ -1100,16 +1090,6 @@ Templates:
Inner: Road
Points: 1,1, 2,1
Start: Clear.R
Segment@1extended:
End: Clear.L
Inner: Road
Points: 2,0, 1,0, 0,0
Start: Road.L
Segment@2extended:
End: Road.R
Inner: Road
Points: 0,1, 1,1, 2,1
Start: Clear.R
Template@95:
Id: 95
Images: d03.des
@@ -1129,16 +1109,6 @@ Templates:
Inner: Road
Points: 0,1, 0,2
Start: Clear.D
Segment@1extended:
End: Clear.U
Inner: Road
Points: 1,2, 1,1, 1,0
Start: Road.U
Segment@2extended:
End: Road.D
Inner: Road
Points: 0,0, 0,1, 0,2
Start: Clear.D
Template@96:
Id: 96
Images: d04.des
@@ -1159,16 +1129,6 @@ Templates:
Inner: Road
Points: 1,1, 0,1
Start: Clear.L
Segment@1extended:
End: Clear.R
Inner: Road
Points: 0,2, 1,2, 2,2
Start: Road.R
Segment@2extended:
End: Road.L
Inner: Road
Points: 2,1, 1,1, 0,1
Start: Clear.L
Template@97:
Id: 97
Images: d05.des

View File

@@ -1143,16 +1143,6 @@ Templates:
Inner: Road
Points: 2,1, 2,0
Start: Clear.U
Segment@1extended:
End: Clear.D
Inner: Road
Points: 1,0, 1,1, 1,2
Start: Road.D
Segment@2extended:
End: Road.U
Inner: Road
Points: 2,2, 2,1, 2,0
Start: Clear.U
Template@94:
Id: 94
Images: d02.sno
@@ -1174,16 +1164,6 @@ Templates:
Inner: Road
Points: 1,1, 2,1
Start: Clear.R
Segment@1extended:
End: Clear.L
Inner: Road
Points: 2,0, 1,0, 0,0
Start: Road.L
Segment@2extended:
End: Road.R
Inner: Road
Points: 0,1, 1,1, 2,1
Start: Clear.R
Template@95:
Id: 95
Images: d03.sno
@@ -1203,16 +1183,6 @@ Templates:
Inner: Road
Points: 0,1, 0,2
Start: Clear.D
Segment@1extended:
End: Clear.U
Inner: Road
Points: 1,2, 1,1, 1,0
Start: Road.U
Segment@2extended:
End: Road.D
Inner: Road
Points: 0,0, 0,1, 0,2
Start: Clear.D
Template@96:
Id: 96
Images: d04.sno
@@ -1233,16 +1203,6 @@ Templates:
Inner: Road
Points: 1,1, 0,1
Start: Clear.L
Segment@1extended:
End: Clear.R
Inner: Road
Points: 0,2, 1,2, 2,2
Start: Road.R
Segment@2extended:
End: Road.L
Inner: Road
Points: 2,1, 1,1, 0,1
Start: Clear.L
Template@97:
Id: 97
Images: d05.sno

View File

@@ -1131,16 +1131,6 @@ Templates:
Inner: Road
Points: 2,1, 2,0
Start: Clear.U
Segment@1extended:
End: Clear.D
Inner: Road
Points: 1,0, 1,1, 1,2
Start: Road.D
Segment@2extended:
End: Road.U
Inner: Road
Points: 2,2, 2,1, 2,0
Start: Clear.U
Template@94:
Id: 94
Images: d02.tem
@@ -1162,16 +1152,6 @@ Templates:
Inner: Road
Points: 1,1, 2,1
Start: Clear.R
Segment@1extended:
End: Clear.L
Inner: Road
Points: 2,0, 1,0, 0,0
Start: Road.L
Segment@2extended:
End: Road.R
Inner: Road
Points: 0,1, 1,1, 2,1
Start: Clear.R
Template@95:
Id: 95
Images: d03.tem
@@ -1191,16 +1171,6 @@ Templates:
Inner: Road
Points: 0,1, 0,2
Start: Clear.D
Segment@1extended:
End: Clear.U
Inner: Road
Points: 1,2, 1,1, 1,0
Start: Road.U
Segment@2extended:
End: Road.D
Inner: Road
Points: 0,0, 0,1, 0,2
Start: Clear.D
Template@96:
Id: 96
Images: d04.tem
@@ -1221,16 +1191,6 @@ Templates:
Inner: Road
Points: 1,1, 0,1
Start: Clear.L
Segment@1extended:
End: Clear.R
Inner: Road
Points: 0,2, 1,2, 2,2
Start: Road.R
Segment@2extended:
End: Road.L
Inner: Road
Points: 2,1, 1,1, 0,1
Start: Clear.L
Template@97:
Id: 97
Images: d05.tem

View File

@@ -1115,16 +1115,6 @@ Templates:
Inner: Road
Points: 2,1, 2,0
Start: Clear.U
Segment@1extended:
End: Clear.D
Inner: Road
Points: 1,0, 1,1, 1,2
Start: Road.D
Segment@2extended:
End: Road.U
Inner: Road
Points: 2,2, 2,1, 2,0
Start: Clear.U
Template@94:
Id: 94
Images: d02.win
@@ -1146,16 +1136,6 @@ Templates:
Inner: Road
Points: 1,1, 2,1
Start: Clear.R
Segment@1extended:
End: Clear.L
Inner: Road
Points: 2,0, 1,0, 0,0
Start: Road.L
Segment@2extended:
End: Road.R
Inner: Road
Points: 0,1, 1,1, 2,1
Start: Clear.R
Template@95:
Id: 95
Images: d03.win
@@ -1175,16 +1155,6 @@ Templates:
Inner: Road
Points: 0,1, 0,2
Start: Clear.D
Segment@1extended:
End: Clear.U
Inner: Road
Points: 1,2, 1,1, 1,0
Start: Road.U
Segment@2extended:
End: Road.D
Inner: Road
Points: 0,0, 0,1, 0,2
Start: Clear.D
Template@96:
Id: 96
Images: d04.win
@@ -1205,16 +1175,6 @@ Templates:
Inner: Road
Points: 1,1, 0,1
Start: Clear.L
Segment@1extended:
End: Clear.R
Inner: Road
Points: 0,2, 1,2, 2,2
Start: Road.R
Segment@2extended:
End: Road.L
Inner: Road
Points: 2,1, 1,1, 0,1
Start: Clear.L
Template@97:
Id: 97
Images: d05.win