From f09959ecba00ceb3b6a1e1bf8a8f005757265828 Mon Sep 17 00:00:00 2001 From: Ashley Newson Date: Sun, 6 Apr 2025 23:24:42 +0100 Subject: [PATCH] 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. --- .../MapGenerator/MatrixUtils.cs | 30 +++ OpenRA.Mods.Common/MapGenerator/TilingPath.cs | 236 +++++++++++++----- .../Traits/World/ExperimentalMapGenerator.cs | 4 + mods/cnc/tilesets/desert.yaml | 40 --- mods/cnc/tilesets/snow.yaml | 40 --- mods/cnc/tilesets/temperat.yaml | 40 --- mods/cnc/tilesets/winter.yaml | 40 --- 7 files changed, 204 insertions(+), 226 deletions(-) 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