diff --git a/OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs b/OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs index b95e1a1479..14e87e99f2 100644 --- a/OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs +++ b/OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs @@ -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; } + + /// + /// Rank all cell values and select the best (greatest compared) value. + /// If there are equally good best candidates, choose one at random. + /// + public static (int2 MPos, T Value) FindRandomBest( + Matrix matrix, + MersenneTwister random, + Comparison comparison) + { + var candidates = new List(); + 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); + } } } diff --git a/OpenRA.Mods.Common/MapGenerator/TilingPath.cs b/OpenRA.Mods.Common/MapGenerator/TilingPath.cs index a1b4059601..671b8b4ccf 100644 --- a/OpenRA.Mods.Common/MapGenerator/TilingPath.cs +++ b/OpenRA.Mods.Common/MapGenerator/TilingPath.cs @@ -212,6 +212,13 @@ namespace OpenRA.Mods.Common.MapGenerator /// public int MinSeparation; + /// + /// 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. + /// + public int MaxEndDeviation; + /// /// Stores start type and direction. /// @@ -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(); - var segmentsByStart = new List>(); - var segmentsByEnd = new List>(); - var costs = new List>(); + var segmentsByStart = new List>(); + var segmentsByEnd = new List>(); + + // 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(size).Fill(MaxCost); + var innerCosts = new List>(); { 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(size).Fill(MaxCost)); + innerCosts.Add(new Matrix(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 - { - new(pathEnd.X + minPoint.X, pathEnd.Y + minPoint.Y) - }; + var resultPoints = new List(); - (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(); - 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(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(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; } + + /// Set MaxEndDeviation. + public TilingPath SetMaxEndDeviation(int maxEndDeviation) + { + MaxEndDeviation = maxEndDeviation; + return this; + } + + /// Allow end point deviation as far as MaxDeviation will allow. + public TilingPath SetAutoEndDeviation() + { + MaxEndDeviation = int.MaxValue; + return this; + } } } diff --git a/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs b/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs index cc97c8f4fb..9459724455 100644 --- a/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs +++ b/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs @@ -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(); diff --git a/mods/cnc/tilesets/desert.yaml b/mods/cnc/tilesets/desert.yaml index 0141818b43..dce8aab34a 100644 --- a/mods/cnc/tilesets/desert.yaml +++ b/mods/cnc/tilesets/desert.yaml @@ -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 diff --git a/mods/cnc/tilesets/snow.yaml b/mods/cnc/tilesets/snow.yaml index ac0bd2937a..7d6de69f25 100644 --- a/mods/cnc/tilesets/snow.yaml +++ b/mods/cnc/tilesets/snow.yaml @@ -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 diff --git a/mods/cnc/tilesets/temperat.yaml b/mods/cnc/tilesets/temperat.yaml index 47549b0537..d8d1342a06 100644 --- a/mods/cnc/tilesets/temperat.yaml +++ b/mods/cnc/tilesets/temperat.yaml @@ -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 diff --git a/mods/cnc/tilesets/winter.yaml b/mods/cnc/tilesets/winter.yaml index ec15ffcc54..dd375c65d0 100644 --- a/mods/cnc/tilesets/winter.yaml +++ b/mods/cnc/tilesets/winter.yaml @@ -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