#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using OpenRA.Primitives;
using OpenRA.Support;
namespace OpenRA.Mods.Common.MapGenerator
{
/// Path to be tiled onto a map using MultiBrushSegments.
public sealed class TilingPath
{
/// Describes the type and direction of the start or end of a TilingPath.
public struct Terminal
{
public string Type;
///
/// Direction to use for this terminal.
/// If the direction here is null, it will be determined automatically later.
///
public Direction? Direction;
///
/// A string which can match the format used by MultiBrushSegment's Start or End.
///
public readonly string SegmentType
{
get
{
var direction =
Direction ?? throw new InvalidOperationException("Direction is null");
return $"{Type}.{direction}";
}
}
public Terminal(string type, Direction? direction)
{
Type = type;
Direction = direction;
}
}
///
/// Describes the permitted start, middle, and end segments/MultiBrushes that can be used
/// to tile a path.
///
public sealed class PermittedSegments
{
public readonly ImmutableArray Start;
public readonly ImmutableArray Inner;
public readonly ImmutableArray End;
public IEnumerable All => Start.Union(Inner).Union(End);
public PermittedSegments(
IEnumerable start,
IEnumerable inner,
IEnumerable end)
{
Start = start.ToImmutableArray();
Inner = inner.ToImmutableArray();
End = end.ToImmutableArray();
}
public PermittedSegments(
IReadOnlyList multiBrushes,
IEnumerable all)
{
var array = all.ToImmutableArray();
Start = array;
Inner = array;
End = array;
}
///
/// Creates a PermittedSegments using only the given types.
///
public static PermittedSegments FromType(
IReadOnlyList multiBrushes,
IEnumerable types)
=> new(multiBrushes, FindSegments(multiBrushes, types));
///
/// Creates a PermittedSegments suitable for a path with given inner and terminal types
/// at the start and end.
///
public static PermittedSegments FromInnerAndTerminalTypes(
IReadOnlyList multiBrushes,
IEnumerable innerTypes,
IEnumerable terminalTypes)
{
var innerTypesArray = innerTypes.ToImmutableArray();
var terminalTypesArray = terminalTypes.ToImmutableArray();
return new(
FindSegments(multiBrushes, terminalTypesArray, innerTypesArray, innerTypesArray),
FindSegments(multiBrushes, innerTypesArray),
FindSegments(multiBrushes, innerTypesArray, innerTypesArray, terminalTypesArray));
}
///
/// Creates a PermittedSegments suitable for a path with given inner and terminal types
/// at the start and end.
///
public static PermittedSegments FromTypes(
IReadOnlyList multiBrushes,
IEnumerable startTypes,
IEnumerable innerTypes,
IEnumerable endTypes)
{
var startTypesArray = startTypes.ToImmutableArray();
var innerTypesArray = innerTypes.ToImmutableArray();
var endTypesArray = endTypes.ToImmutableArray();
return new(
FindSegments(multiBrushes, startTypesArray, innerTypesArray, innerTypesArray),
FindSegments(multiBrushes, innerTypesArray),
FindSegments(multiBrushes, innerTypesArray, innerTypesArray, endTypesArray));
}
///
/// Equivalent to FindSegments(multiBrushes, types, types, types).
///
public static IEnumerable FindSegments(
IReadOnlyList multiBrushes,
IEnumerable types)
{
var array = types.ToImmutableArray();
return FindSegments(multiBrushes, array, array, array);
}
///
/// Filter MultiBrushes to segments that use the given start, inner, and end types.
///
public static IEnumerable FindSegments(
IReadOnlyList multiBrushes,
IEnumerable startTypes,
IEnumerable innerTypes,
IEnumerable endTypes)
{
var filtered = new List();
var startTypesArray = startTypes.ToImmutableArray();
var innerTypesArray = innerTypes.ToImmutableArray();
var endTypesArray = endTypes.ToImmutableArray();
foreach (var multiBrush in multiBrushes)
if (startTypesArray.Any(multiBrush.Segment.HasStartType) &&
innerTypesArray.Any(multiBrush.Segment.HasInnerType) &&
endTypesArray.Any(multiBrush.Segment.HasEndType))
{
filtered.Add(multiBrush);
}
return [.. filtered];
}
}
public Map Map;
///
///
/// Target point sequence to fit MultiBrushSegments to. Whether these CPos positions
/// represent cell corners or cell centers is dependent on the system used by the path's
/// PermittedSegments' MultiBrushSegments.
///
///
/// If null, Tiling will be a no-op. If non-null, must have at least two points.
///
///
/// A loop must have the start and end points equal.
///
///
public CPos[] Points;
///
/// Maximum permitted Chebyshev distance that layed MultiBrushSegments may be from the
/// specified points.
///
public int MaxDeviation;
///
/// Determines how much corner-cutting is allowed.
/// A value of zero will result in a value being derived from MaxDeviation.
///
public int MaxSkip;
///
/// Increases separation between permitted tiling regions of different parts of the path.
///
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.
///
public Terminal Start;
///
/// Stores end type and direction.
///
public Terminal End;
public PermittedSegments Brushes;
/// Whether the start and end points are the same.
public bool IsLoop
{
get => Points != null && Points[0] == Points[^1];
}
public TilingPath(
Map map,
CPos[] points,
int maxDeviation,
string startType,
string endType,
PermittedSegments permittedTemplates)
{
Map = map;
Points = points;
MaxDeviation = maxDeviation;
MaxSkip = 0;
MinSeparation = 0;
MaxEndDeviation = 0;
Start = new Terminal(startType, null);
End = new Terminal(endType, null);
Brushes = permittedTemplates;
}
///
/// Convenience method to create TilingPaths using common settings.
/// Start and end terminal types are the same.
/// PermittedSegments are derived from brushes and inner/terminal types.
/// Loops will automatically use only the inner type.
/// Uses automatic end deviation and loop optimization.
///
public static TilingPath QuickCreate(
Map map,
IReadOnlyList brushes,
CPos[] points,
int maxDeviation,
string innerSegmentType,
string terminalSegmentType)
{
var nonLoopedRoadPermittedTemplates =
PermittedSegments.FromInnerAndTerminalTypes(
brushes, [innerSegmentType], [terminalSegmentType]);
var loopedRoadPermittedTemplates =
PermittedSegments.FromType(brushes, [innerSegmentType]);
var isLoop = points[0] == points[^1];
TilingPath path;
if (isLoop)
path = new TilingPath(
map,
points,
maxDeviation,
innerSegmentType,
innerSegmentType,
loopedRoadPermittedTemplates);
else
path = new TilingPath(
map,
points,
maxDeviation,
terminalSegmentType,
terminalSegmentType,
nonLoopedRoadPermittedTemplates);
path
.SetAutoEndDeviation()
.OptimizeLoop();
return path;
}
sealed class TilingSegment
{
public readonly MultiBrush MultiBrush;
public readonly int StartTypeId;
public readonly int EndTypeId;
public readonly CVec Offset;
public readonly CVec Moves;
public readonly CVec[] RelativePoints;
public readonly Direction EndDirection;
public TilingSegment(MultiBrush multiBrush, int startId, int endId)
{
MultiBrush = multiBrush;
StartTypeId = startId;
EndTypeId = endId;
Offset = multiBrush.Segment.Points[0];
Moves = multiBrush.Segment.Points[^1] - Offset;
RelativePoints = multiBrush.Segment.Points
.Select(p => p - multiBrush.Segment.Points[0])
.ToArray();
EndDirection = multiBrush.Segment.EndDirection;
for (var i = 0; i < RelativePoints.Length - 1; i++)
{
var direction = DirectionExts.FromCVec(RelativePoints[i + 1] - RelativePoints[i]);
if (direction == Direction.None)
throw new ArgumentException("MultiBrushSegment has duplicate points in sequence");
}
}
}
///
///
/// Attempt to tile the given path, producing a new MultiBrush if the path could be tiled,
/// or null if the path could not be tiled within constraints.
///
///
/// The resulting MultiBrush is created from stitching MultiBrushes from the
/// PermittedSegments together, and will contain a segment that represents the stitched
/// segments of the constituent MultiBrushes.
///
///
public MultiBrush Tile(MersenneTwister random)
{
// This is essentially a Dijkstra's algorithm best-first search.
//
// The search is performed over a 3-dimensional space: (x, y, connection type).
// Connection types correspond to the .Start or .End values of MultiBrushSegments.
//
// The best found costs of the nodes in this space are stored as an array of matrices.
// There is a matrix for each possible connection type, and each matrix stores the
// (current) best costs at the (x, y) locations for that given connection type.
//
// The directed edges between the nodes of this 3-dimensional space are defined by the
// MultiBrushSegments within the permitted set of segments. For example, a segment
// defined as
//
// Segment:
// Start: Beach.L
// End: Beach.D
// Points: 3,1, 2,1, 2,2, 2,3
//
// may connect a node from (10, 10) in the "Beach.L" matrix to node (9, 12) in the
// "Beach.D" matrix. (The overall point displacement is (2,3) - (3,1) = (-1, +2))
//
// The cost of a transition/link/edge between nodes is defined by how well the
// MultiBrushSegment fits the path (how little "deviation" is accumulates). However, in
// order for a transition to be allowed at all, it must satisfy some constraints:
//
// - It must not regress backward along the path (but no immediate progress is OK).
// - If it makes exactly zero progress, it must not end facing towards overall
// decreasing progress or strictly neutral progress (neither earliest or latest
// closest points differ).
// - It must not deviate at any point in the segment beyond MaxDeviation from the path.
// - It must not skip to much later path points (which may be within MaxDeviation).
//
// Progress is measured as a combo of both the earliest and latest closest path points.
//
// 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
// segment which connects back to a previous node where the difference in cost is
// that of the segment's cost, implying that the previous node is on an
// optimal path towards the end node. This process repeats until the start node is
// reached, merging MultiBrushes into a result along the way.
//
// Note that this algorithm makes a few (reasonable) assumptions about the shapes of
// MultiBrushes, such as that they don't individually snake around too much. The actual
// tiles of a MultiBrush are ignored during the search, with only the segment being
// used to calculate transition cost and validity.
if (Points == null)
return null;
var start = Start;
var end = End;
start.Direction ??= DirectionExts.FromCVec(Points[1] - Points[0]);
end.Direction ??= DirectionExts.FromCVec(IsLoop ? Points[1] - Points[0] : Points[^1] - Points[^2]);
var maxSkip = MaxSkip > 0 ? MaxSkip : (2 * MaxDeviation + 1);
var scanRange = MaxDeviation + MinSeparation;
var minPoint = new CPos(
Points.Min(p => p.X) - scanRange,
Points.Min(p => p.Y) - scanRange);
var maxPoint = new CPos(
Points.Max(p => p.X) + scanRange,
Points.Max(p => p.Y) + scanRange);
var points = Points
.Select(point => point - minPoint)
.ToArray();
var isLoop = IsLoop;
// grid points (not squares), so these are offset 0.5 from tile centers.
var size = new int2(1 + maxPoint.X - minPoint.X, 1 + maxPoint.Y - minPoint.Y);
var sizeXY = size.X * size.Y;
const int OverDeviation = int.MaxValue;
const int InvalidProgress = int.MaxValue;
// How far away from the path this point is.
var deviations = new Matrix(size).Fill(OverDeviation);
var lowProgress = new Matrix(size).Fill(InvalidProgress);
var highProgress = new Matrix(size).Fill(InvalidProgress);
var progressModulus = IsLoop ? points.Length - 1 : points.Length;
// The following only apply to looped paths
var forwardProgressLimit = (progressModulus + 1) / 2;
var backwardProgressLimit = progressModulus / 2;
// MinValue essentially means "never match me".
var oppositeProgress =
(IsLoop && forwardProgressLimit == backwardProgressLimit)
? 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)
{
var progress = (progressModulus + to - from) % progressModulus;
if (progress < forwardProgressLimit)
return progress;
else if (progress > backwardProgressLimit)
return progress - progressModulus;
else
return oppositeProgress;
}
else
{
return to - from;
}
}
{
var progressSeeds = new List<(int2, int)>();
for (var pointI = 0; pointI < progressModulus; pointI++)
{
var point = points[pointI];
lowProgress[point.X, point.Y] = pointI;
highProgress[point.X, point.Y] = pointI;
progressSeeds.Add((new int2(point.X, point.Y), 0));
}
(int Low, int High) FindLowAndHigh(List 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++)
if (Progress(values[i], values[i + 1]) < 0)
return (values[i + 1], values[i]);
return (InvalidProgress, InvalidProgress);
}
else
{
return (values[0], values[^1]);
}
}
var lows = new List(8);
var highs = new List(8);
int? ProgressFiller(int2 xy, int deviation)
{
if (deviations[xy] != OverDeviation)
return null;
deviations[xy] = deviation;
// low and high progress is preset for 0-deviation.
if (deviation == 0)
return 1;
lows.Clear();
highs.Clear();
foreach (var offset in DirectionExts.Spread8)
{
var neighbor = xy + offset;
if (!deviations.ContainsXY(neighbor) ||
deviations[neighbor] >= deviation ||
lowProgress[neighbor] == InvalidProgress ||
highProgress[neighbor] == InvalidProgress)
{
continue;
}
lows.Add(lowProgress[neighbor]);
highs.Add(highProgress[neighbor]);
}
lows.Sort();
highs.Sort();
(lowProgress[xy], _) = FindLowAndHigh(lows);
(_, highProgress[xy]) = FindLowAndHigh(highs);
if (deviation == scanRange)
return null;
return deviation + 1;
}
MatrixUtils.FloodFill(
size,
progressSeeds,
ProgressFiller,
DirectionExts.Spread8);
var separationSeeds = new List<(int2, int)>();
for (var y = 0; y < size.Y; y++)
for (var x = 0; x < size.X; x++)
{
var xy = new int2(x, y);
var low = lowProgress[xy];
var high = highProgress[xy];
if (low == InvalidProgress ||
high == InvalidProgress)
{
separationSeeds.Add((xy, MinSeparation));
continue;
}
if (MinSeparation > 0)
{
foreach (var offset in DirectionExts.Spread8)
{
var neighbor = xy + offset;
if (!deviations.ContainsXY(neighbor) ||
Math.Abs(Progress(low, lowProgress[neighbor])) > maxSkip ||
Math.Abs(Progress(high, highProgress[neighbor])) > maxSkip)
{
separationSeeds.Add((xy, MinSeparation - 1));
break;
}
}
// Last so that any greater range seeds take priority.
if (deviations[xy] > MaxDeviation)
separationSeeds.Add((xy, 0));
}
}
int? SeparationFiller(int2 xy, int range)
{
if (deviations[xy] == 0 || deviations[xy] == OverDeviation)
return null;
deviations[xy] = OverDeviation;
if (range == 0)
return null;
return range - 1;
}
MatrixUtils.FloodFill(
size,
separationSeeds,
SeparationFiller,
DirectionExts.Spread8);
}
var pathStart = points[0];
var pathEnd = points[^1];
var orderedPermittedBrushes = Brushes.All.ToImmutableArray();
var permittedStartBrushes = Brushes.Start.ToHashSet();
var permittedInnerBrushes = Brushes.Inner.ToHashSet();
var permittedEndBrushes = Brushes.End.ToHashSet();
const int MaxCost = int.MaxValue;
var segmentTypeToId = new Dictionary();
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)
{
if (segmentTypeToId.ContainsKey(type))
return;
var newId = segmentTypeToId.Count;
segmentTypeToId.Add(type, newId);
segmentsByStart.Add([]);
segmentsByEnd.Add([]);
innerCosts.Add(new Matrix(size).Fill(MaxCost));
}
foreach (var multiBrush in orderedPermittedBrushes)
{
var segment = multiBrush.Segment;
RegisterSegmentType(segment.Start);
RegisterSegmentType(segment.End);
var startTypeId = segmentTypeToId[segment.Start];
var endTypeId = segmentTypeToId[segment.End];
var tilePathSegment = new TilingSegment(multiBrush, startTypeId, endTypeId);
var tuple = (
tilePathSegment,
permittedStartBrushes.Contains(multiBrush) && segment.Start == start.SegmentType,
permittedInnerBrushes.Contains(multiBrush),
permittedEndBrushes.Contains(multiBrush) && segment.End == end.SegmentType);
segmentsByStart[startTypeId].Add(tuple);
segmentsByEnd[endTypeId].Add(tuple);
}
}
var totalTypeIds = segmentTypeToId.Count;
var priorities = new PriorityArray(totalTypeIds * size.X * size.Y, MaxCost);
void SetPriorityAt(int typeId, CVec pos, int priority)
=> priorities[typeId * sizeXY + pos.Y * size.X + pos.X] = priority;
(int TypeId, CVec Pos, int Priority) GetNextPriority()
{
var index = priorities.GetMinIndex();
var priority = priorities[index];
var typeId = index / sizeXY;
var xy = index % sizeXY;
return (typeId, new CVec(xy % size.X, xy / size.X), priority);
}
if (!segmentTypeToId.TryGetValue(start.SegmentType, out var pathStartTypeId))
return null;
if (!segmentTypeToId.TryGetValue(end.SegmentType, out var pathEndTypeId))
return null;
// Lower (closer to zero) costs are better matches.
// MaxScore means totally unacceptable.
int ScoreSegment(TilingSegment segment, CVec from)
{
var to = from + segment.Moves;
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;
var lowProgressionAcc = 0;
var highProgressionAcc = 0;
var lastPointI = segment.RelativePoints.Length - 1;
for (var pointI = 0; pointI <= lastPointI; pointI++)
{
var point = from + segment.RelativePoints[pointI];
if (!deviations.ContainsXY(point.X, point.Y) || deviations[point.X, point.Y] == OverDeviation)
{
// Point escapes bounds or is in an excluded position.
return MaxCost;
}
if (pointI < lastPointI)
{
var pointNext = from + segment.RelativePoints[pointI + 1];
if (!deviations.ContainsXY(pointNext.X, pointNext.Y) || deviations[pointNext.X, pointNext.Y] == OverDeviation)
{
// Next point escapes bounds or is in an excluded position.
return MaxCost;
}
var lowProgression = Progress(lowProgress[point.X, point.Y], lowProgress[pointNext.X, pointNext.Y]);
var highProgression = Progress(highProgress[point.X, point.Y], highProgress[pointNext.X, pointNext.Y]);
if (Math.Abs(lowProgression) > maxSkip ||
Math.Abs(highProgression) > maxSkip)
{
// Fails skip rule.
return MaxCost;
}
lowProgressionAcc += lowProgression;
highProgressionAcc += highProgression;
}
// pointI > 0 is needed to avoid double-counting the segments's start with the
// previous one's end.
if (pointI > 0)
deviationAcc += deviations[point.X, point.Y];
}
if (lowProgressionAcc < 0 || highProgressionAcc < 0)
{
// Fails progression rule.
return MaxCost;
}
// If it's a zero-progress segment without deviation, only allow it if it directs
// towards a positive progression.
if (lowProgressionAcc == 0 && highProgressionAcc == 0)
{
var point = to;
var pointNext = to + segment.EndDirection.ToCVec();
if (!deviations.ContainsXY(pointNext.X, pointNext.Y) || deviations[pointNext.X, pointNext.Y] == OverDeviation)
{
// Projected point escapes bounds or is in an excluded position.
return MaxCost;
}
lowProgressionAcc = Progress(lowProgress[point.X, point.Y], lowProgress[pointNext.X, pointNext.Y]);
highProgressionAcc = Progress(highProgress[point.X, point.Y], highProgress[pointNext.X, pointNext.Y]);
if ((lowProgressionAcc <= 0 && highProgressionAcc <= 0) || lowProgressionAcc + highProgressionAcc < 0)
return MaxCost;
}
// Satisfies all requirements.
return deviationAcc;
}
void UpdateFrom(CVec from, int fromTypeId, bool isForStart)
{
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;
// Most likely to fail. Check first.
if (deviations[to.X, to.Y] == OverDeviation)
{
// End escapes bounds.
continue;
}
var segmentCost = ScoreSegment(segment, from);
if (segmentCost == MaxCost)
continue;
var toCost = fromCost + segmentCost;
var toTypeId = segment.EndTypeId;
if ((canStart || canInner) && toCost < innerCosts[toTypeId][to.X, to.Y])
{
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);
}
UpdateFrom(pathStart, pathStartTypeId, true);
while (true)
{
var (fromTypeId, from, priority) = GetNextPriority();
if (priority == MaxCost)
break;
UpdateFrom(from, fromTypeId, false);
}
// Trace back and update tiles
var resultPoints = new List();
var compositeBrush = new MultiBrush();
(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, 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;
// Most likely to fail. Check first.
if (deviations[from.X, from.Y] == OverDeviation)
{
// Start escapes bounds.
continue;
}
var segmentCost = ScoreSegment(segment, from);
if (segmentCost == MaxCost)
continue;
var fromCost = toCost - segmentCost;
var requiredFromCost =
mustStart ? 0 : innerCosts[segment.StartTypeId][from.X, from.Y];
if (fromCost == requiredFromCost)
candidates.Add(segment);
}
Debug.Assert(candidates.Count >= 1, "TraceBack didn't find an original route");
var weights = candidates
.Select(c => c.MultiBrush.Weight)
.ToArray();
var chosenSegment = candidates[random.PickWeighted(weights)];
var chosenFrom = to - chosenSegment.Moves;
compositeBrush.MergeFrom(
chosenSegment.MultiBrush,
chosenFrom - chosenSegment.Offset + minPoint - CPos.Zero,
Map.Grid.Type);
// Skip end point as it is recorded in the previous segment.
for (var i = chosenSegment.RelativePoints.Length - 2; i >= 0; i--)
{
var point = chosenFrom + chosenSegment.RelativePoints[i] + minPoint - CPos.Zero;
resultPoints.Add(point);
}
return (chosenFrom, chosenSegment.StartTypeId);
}
{
var toTypeId = pathEndTypeId;
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;
// 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,
DirectionExts.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 || toTypeId != pathStartTypeId)
(to, toTypeId) = TraceBackStep(to, toTypeId, false);
}
// Traced back in reverse, so reverse the reversal.
resultPoints.Reverse();
var compositeSegment = new MultiBrushSegment(
start.SegmentType,
"(Tiled Path)",
end.SegmentType,
[.. resultPoints]);
compositeBrush.ReplaceSegment(compositeSegment);
return compositeBrush;
}
///
///
/// 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.
///
///
/// Returns the object being called on.
///
///
public TilingPath InertiallyExtend(int extensionLength, int inertialRange)
{
Points = InertiallyExtendPathPoints(Points, extensionLength, inertialRange);
return this;
}
///
/// 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 = DirectionExts.FromCVecNonDiagonal(points[inertialRange] - points[0]);
var ed = DirectionExts.FromCVecNonDiagonal(points[^1] - points[^(inertialRange + 1)]);
var newPoints = new CPos[points.Length + extensionLength * 2];
for (var i = 0; i < extensionLength; i++)
newPoints[i] = points[0] - sd.ToCVec() * (extensionLength - i);
Array.Copy(points, 0, newPoints, extensionLength, points.Length);
for (var i = 0; i < extensionLength; i++)
newPoints[extensionLength + points.Length + i] = points[^1] + ed.ToCVec() * (i + 1);
return newPoints;
}
///
///
/// For map edge-connected (non-loop) starts/ends, the path is extended beyond the edge.
/// For loops or paths which don't connect to the map edge, no change is applied.
///
///
/// For the purposes of this function, the map edges are defined as the borders of a
/// minimal CPos-aligned rectangle covering the entire map. These are not the true edges of
/// a RectangularIsometric map.
///
///
/// Starts/ends which are corner-connected or already extend beyond the edge are unaltered.
///
///
/// Returns the object being called on.
///
///
public TilingPath ExtendEdge(int extensionLength)
{
Points = ExtendEdgePathPoints(Points, CellLayerUtils.CellBounds(Map), extensionLength);
return this;
}
///
///
/// For bounds edge-connected (non-loop) starts/ends, the path is extended beyond the edge.
/// For loops or paths which don't connect to the edges, the input points are returned
/// unaltered.
///
///
/// Starts/ends which are corner-connected or already extend beyond the edge are unaltered.
///
///
public static CPos[] ExtendEdgePathPoints(CPos[] points, Rectangle bounds, int extensionLength)
{
if (points == null)
return null;
if (points[0] == points[^1])
{
// Is a loop.
return points;
}
var left = bounds.Left;
var top = bounds.Top;
var right = bounds.Right;
var bottom = bounds.Bottom;
CPos[] Extend(CPos point)
{
var ox = (point.X == left) ? -1
: (point.X == right) ? 1
: 0;
var oy = (point.Y == top) ? -1
: (point.Y == bottom) ? 1
: 0;
if (ox == oy)
{
// We're either not on an edge or we're at a corner, so don't extend.
return [];
}
var offset = new CVec(ox, oy);
var extension = new CPos[extensionLength];
var newPoint = point;
for (var i = 0; i < extensionLength; i++)
{
newPoint += offset;
extension[i] = newPoint;
}
return extension;
}
// Open paths. Extend if beyond edges.
var startExt = Extend(points[0]).Reverse().ToArray();
var endExt = Extend(points[^1]);
// [...startExt, ...points, ...endExt];
var tweaked = new CPos[points.Length + startExt.Length + endExt.Length];
Array.Copy(startExt, 0, tweaked, 0, startExt.Length);
Array.Copy(points, 0, tweaked, startExt.Length, points.Length);
Array.Copy(endExt, 0, tweaked, points.Length + startExt.Length, endExt.Length);
return tweaked;
}
///
///
/// For loops, points are rotated such that the start/end reside in the longest straight.
/// For non-loops, the input points are returned unaltered.
///
///
/// Returns the object being called on.
///
///
public TilingPath OptimizeLoop()
{
Points = OptimizeLoopPathPoints(Points);
return this;
}
///
/// For loops, points are rotated such that the start/end reside in the longest straight.
/// For non-loops, the input points are returned unaltered.
///
public static CPos[] OptimizeLoopPathPoints(CPos[] points)
{
if (points == null)
return null;
if (points[0] == points[^1])
{
// Closed loop. Find the longest straight
// (nrlen excludes the repeated point at the end.)
var nrlen = points.Length - 1;
var prevDim = -1;
var scanStart = -1;
var bestScore = -1;
var bestBend = -1;
var prevBend = -1;
var prevI = 0;
for (var i = 1; ; i++)
{
if (i == nrlen)
i = 0;
var dim = points[i].X == points[prevI].X ? 1 : 0;
if (prevDim != -1 && prevDim != dim)
{
if (scanStart == -1)
{
// This is technically just after the bend. But that's fine.
scanStart = i;
}
else
{
var score = prevI - prevBend;
if (score < 0)
score += nrlen;
if (score > bestScore)
{
bestBend = prevBend;
bestScore = score;
}
if (i == scanStart)
break;
}
prevBend = prevI;
}
prevDim = dim;
prevI = i;
}
var favouritePoint = (bestBend + (bestScore >> 1)) % nrlen;
// Repeat the start at the end.
// [...points.slice(favouritePoint, nrlen), ...points.slice(0, favouritePoint + 1)];
var tweaked = new CPos[points.Length];
Array.Copy(points, favouritePoint, tweaked, 0, nrlen - favouritePoint);
Array.Copy(points, 0, tweaked, nrlen - favouritePoint, favouritePoint + 1);
return tweaked;
}
else
{
return points;
}
}
///
///
/// Shrink a path by a given amount at both ends. If the number of points in the path drops
/// below minimumLength, the path is nullified.
///
///
/// If a loop is provided, the path is not shrunk, but the minimumLength requirement still
/// holds.
///
///
/// Returns the object being called on.
///
///
public TilingPath Shrink(int shrinkBy, int minimumLength)
{
Points = ShrinkPathPoints(Points, shrinkBy, minimumLength);
return this;
}
///
///
/// Shrink a path by a given amount at both ends. If the number of points in the path drops
/// below minimumLength, null is returned.
///
///
/// If a loop is provided, the path is not shrunk, but the minimumLength requirement still
/// holds.
///
///
public static CPos[] ShrinkPathPoints(CPos[] points, int shrinkBy, int minimumLength)
{
if (points == null)
return null;
if (minimumLength <= 1)
throw new ArgumentException("minimumLength must be greater than 1");
if (points[0] == points[^1])
{
// Loop.
if (points.Length < minimumLength)
return null;
return points[0..^0];
}
if (points.Length < shrinkBy * 2 + minimumLength)
return null;
return points[shrinkBy..(points.Length - shrinkBy)];
}
///
///
/// Takes a path and normalizes its progression direction around the map center.
/// Normalized but opposing paths rotate around the center in the same direction.
///
///
/// The measureFromCenter function must convert CVec positions to WVec offsets from the map
/// center.
///
///
public TilingPath ChirallyNormalize(Func measureFromCenter)
{
Points = ChirallyNormalizePathPoints(Points, measureFromCenter);
return this;
}
///
///
/// Takes a path and normalizes its progression direction around the map center.
/// 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.
///
///
public static CPos[] ChirallyNormalizePathPoints(CPos[] points, Func measureFromCenter)
{
if (points == null || points.Length < 2)
return points;
var normalized = (CPos[])points.Clone();
var start = points[0];
var end = points[^1];
if (start == end)
{
// 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;
}
}
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
{
// 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;
// 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;
}
if (ShouldReverse(start, end))
Array.Reverse(normalized);
}
return normalized;
}
///
///
/// Retains paths which have no points in common with earlier (previous and retained) paths
/// from the input.
///
///
/// The underlying point sequences are NOT cloned.
///
///
/// All input sequences must be non-null.
///
///
public static CPos[][] RetainDisjointPaths(IEnumerable inputs)
{
var outputs = new List();
var lookup = new HashSet();
foreach (var points in inputs)
{
var retain = true;
foreach (var point in points)
{
if (lookup.Contains(point))
{
retain = false;
break;
}
}
if (retain)
{
outputs.Add(points);
foreach (var point in points)
lookup.Add(point);
}
}
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 (DirectionExts.FromCVecNonDiagonal(offset).ToCVec() != offset)
return false;
lastPoint = points[i];
}
return true;
}
/// Applies StraightenEndsPathPoints to this TilingPath, returning this.
public TilingPath StraightenEnds(
int shrink,
int grow,
int minimumLength,
int growthInertialRange)
{
Points = StraightenEndsPathPoints(
Points,
CellLayerUtils.CellBounds(Map),
shrink,
grow,
minimumLength,
growthInertialRange);
return this;
}
///
/// Straighten the start and end of a path by shrinking and regrowing a straight section.
///
/// Points of the path.
/// Map bounds, used to identify paths touching edges.
/// Distance to shrink path ends (before regrowing them).
/// Distance to regrow path ends with straightening (after shrinking).
/// The minimum length (after shrinking, before growth) that paths may be.
/// How many points are used to decide the regrowth direction.
public static CPos[] StraightenEndsPathPoints(
CPos[] points,
Rectangle bounds,
int shrink,
int grow,
int minimumLength,
int growthInertialRange)
{
points = ExtendEdgePathPoints(points, bounds, 2 * shrink + minimumLength);
points = ShrinkPathPoints(points, shrink, minimumLength);
points = InertiallyExtendPathPoints(points, grow, growthInertialRange);
return points;
}
/// 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;
}
}
}