Files
OpenRA/OpenRA.Mods.Common/MapGenerator/TilingPath.cs
Ashley Newson 417f787294 Add experimental RA procedural map generator
Add an experimental procedural map generator for the Red Alert mod,
along with supporting code that may assist in the development of map
generators for other mods.

Map generation may be accessed as a tool in the Map Editor. This change
does not presently introduce direct lobby options for generated maps.

Features:

- Terrain with land, water, beaches, cliffs, roads, debris, and trees.
- Placement of mpspawns, neutral buildings, and resources.
- Rotational and mirror symmetry options.
- Various configurable parameters with presets.
- Deterministic with configurable seed.
- Performant.
2025-01-09 16:47:10 +02:00

1196 lines
36 KiB
C#

#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.Mods.Common.Terrain;
using OpenRA.Primitives;
using OpenRA.Support;
namespace OpenRA.Mods.Common.MapGenerator
{
/// <summary>Path to be tiled onto a map using TemplateSegments.</summary>
public sealed class TilingPath
{
/// <summary>Describes the type and direction of the start or end of a TilingPath.</summary>
public struct Terminal
{
public string Type;
/// <summary>
/// Direction to use for this terminal.
/// If the direction here is null, it will be determined automatically later.
/// </summary>
public int? Direction;
/// <summary>
/// A string which can match the format used by
/// OpenRA.Mods.Common.Terrain.TemplateSegment's Start or End.
/// </summary>
public readonly string SegmentType
{
get
{
var direction =
Direction ?? throw new InvalidOperationException("Direction is null");
return $"{Type}.{MapGenerator.Direction.ToString(direction)}";
}
}
public Terminal(string type, int? direction)
{
Type = type;
Direction = direction;
}
}
/// <summary>
/// Describes the permitted start, middle, and end segments/templates that can be used to
/// tile the path.
/// </summary>
public sealed class PermittedSegments
{
public readonly ITemplatedTerrainInfo TemplatedTerrainInfo;
public readonly ImmutableArray<TemplateSegment> Start;
public readonly ImmutableArray<TemplateSegment> Inner;
public readonly ImmutableArray<TemplateSegment> End;
public IEnumerable<TemplateSegment> All => Start.Union(Inner).Union(End);
public PermittedSegments(
ITemplatedTerrainInfo templatedTerrainInfo,
IEnumerable<TemplateSegment> start,
IEnumerable<TemplateSegment> inner,
IEnumerable<TemplateSegment> end)
{
TemplatedTerrainInfo = templatedTerrainInfo;
Start = start.ToImmutableArray();
Inner = inner.ToImmutableArray();
End = end.ToImmutableArray();
}
public PermittedSegments(
ITemplatedTerrainInfo templatedTerrainInfo,
IEnumerable<TemplateSegment> all)
{
TemplatedTerrainInfo = templatedTerrainInfo;
var array = all.ToImmutableArray();
Start = array;
Inner = array;
End = array;
}
/// <summary>
/// Creates a PermittedSegments using only the given types.
/// </summary>
public static PermittedSegments FromType(
ITemplatedTerrainInfo templatedTerrainInfo,
IEnumerable<string> types)
=> new(templatedTerrainInfo, FindSegments(templatedTerrainInfo, types));
/// <summary>
/// Creates a PermittedSegments suitable for a path with given inner and terminal types
/// at the start and end.
/// </summary>
public static PermittedSegments FromInnerAndTerminalTypes(
ITemplatedTerrainInfo templatedTerrainInfo,
IEnumerable<string> innerTypes,
IEnumerable<string> terminalTypes)
{
var innerTypesArray = innerTypes.ToImmutableArray();
var terminalTypesArray = terminalTypes.ToImmutableArray();
return new(
templatedTerrainInfo,
FindSegments(templatedTerrainInfo, terminalTypesArray, innerTypesArray, innerTypesArray),
FindSegments(templatedTerrainInfo, innerTypesArray),
FindSegments(templatedTerrainInfo, innerTypesArray, innerTypesArray, terminalTypesArray));
}
/// <summary>
/// Equivalent to FindSegments(templatedTerrainInfo, types, types, types).
/// </summary>
public static IEnumerable<TemplateSegment> FindSegments(
ITemplatedTerrainInfo templatedTerrainInfo,
IEnumerable<string> types)
{
var array = types.ToImmutableArray();
return FindSegments(templatedTerrainInfo, array, array, array);
}
/// <summary>
/// Find templates that use some combination of the given start, inner, and end types.
/// </summary>
public static IEnumerable<TemplateSegment> FindSegments(
ITemplatedTerrainInfo templatedTerrainInfo,
IEnumerable<string> startTypes,
IEnumerable<string> innerTypes,
IEnumerable<string> endTypes)
{
var templateSegments = new List<TemplateSegment>();
foreach (var templateInfo in templatedTerrainInfo.Templates.Values.OrderBy(tti => tti.Id))
foreach (var segment in templateInfo.Segments)
{
if (startTypes.Any(segment.HasStartType) &&
innerTypes.Any(segment.HasInnerType) &&
endTypes.Any(segment.HasEndType))
{
templateSegments.Add(segment);
}
}
return templateSegments.ToArray();
}
/// <summary>
/// Returns all possible templates that could be layed, ordered by template id.
/// </summary>
public IEnumerable<TerrainTemplateInfo> PossibleTemplates()
{
var templates = new List<TerrainTemplateInfo>();
var segments = Start.Union(Inner).Union(End).ToHashSet();
foreach (var template in TemplatedTerrainInfo.Templates.Values.OrderBy(tti => tti.Id))
if (template.Segments.Any(segment => segments.Contains(segment)))
templates.Add(template);
return templates;
}
/// <summary>
/// Returns all possible tiles that could be layed, ordered by template id, tile index.
/// </summary>
public IEnumerable<TerrainTile> PossibleTiles()
{
var tiles = new List<TerrainTile>();
foreach (var template in PossibleTemplates())
for (var index = 0; index < template.TilesCount; index++)
if (template[index] != null)
tiles.Add(new TerrainTile(template.Id, (byte)index));
return tiles;
}
}
public Map Map;
/// <summary>
/// <para>
/// Target point sequence to fit TemplateSegments to. Whether these CPos positions
/// represent cell corners or cell centers is dependent on the system used by the path's
/// PermittedSegments' TemplateSegments.
/// </para>
/// <para>
/// If null, Tiling will be a no-op. If non-null, must have at least two points.
/// </para>
/// <para>
/// A loop must have the start and end points equal.
/// </para>
/// </summary>
public CPos[] Points;
/// <summary>
/// Maximum permitted Chebychev distance that layed TemplateSegments may be from the
/// specified points.
/// </summary>
public int MaxDeviation;
/// <summary>
/// Determines how much corner-cutting is allowed.
/// A value of zero will result in a value being derived from MaxDeviation.
/// </summary>
public int MaxSkip;
/// <summary>
/// Increases separation between permitted tiling regions of different parts of the path.
/// </summary>
public int MinSeparation;
/// <summary>
/// Stores start type and direction.
/// </summary>
public Terminal Start;
/// <summary>
/// Stores end type and direction.
/// </summary>
public Terminal End;
public PermittedSegments Segments;
/// <summary>Whether the start and end points are the same.</summary>
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;
Start = new Terminal(startType, null);
End = new Terminal(endType, null);
Segments = permittedTemplates;
}
sealed class TilingSegment
{
public readonly TerrainTemplateInfo TemplateInfo;
public readonly TemplateSegment TemplateSegment;
public readonly int StartTypeId;
public readonly int EndTypeId;
public readonly CVec Offset;
public readonly CVec Moves;
public readonly CVec[] RelativePoints;
public readonly int[] Directions;
public readonly int[] DirectionMasks;
public readonly int[] ReverseDirectionMasks;
public TilingSegment(TerrainTemplateInfo templateInfo, TemplateSegment templateSegment, int startId, int endId)
{
TemplateInfo = templateInfo;
TemplateSegment = templateSegment;
StartTypeId = startId;
EndTypeId = endId;
Offset = templateSegment.Points[0];
Moves = templateSegment.Points[^1] - Offset;
RelativePoints = templateSegment.Points
.Select(p => p - templateSegment.Points[0])
.ToArray();
Directions = new int[RelativePoints.Length];
DirectionMasks = new int[RelativePoints.Length];
ReverseDirectionMasks = new int[RelativePoints.Length];
// Last point has no direction.
Directions[^1] = Direction.None;
DirectionMasks[^1] = 0;
ReverseDirectionMasks[^1] = 0;
for (var i = 0; i < RelativePoints.Length - 1; i++)
{
var direction = Direction.FromCVec(RelativePoints[i + 1] - RelativePoints[i]);
if (direction == Direction.None)
throw new ArgumentException("TemplateSegment has duplicate points in sequence");
Directions[i] = direction;
DirectionMasks[i] = 1 << direction;
ReverseDirectionMasks[i] = 1 << Direction.Reverse(direction);
}
}
}
/// <summary>
/// <para>
/// Attempt to tile the given path onto a map.
/// </para>
/// <para>
/// If the path could be tiled, returns the sequence of points actually traversed by the
/// chosen TemplateSegments. Returns null if the path could not be tiled within constraints.
/// </para>
/// </summary>
public CPos[] 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 TemplateSegments.
//
// 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
// TemplateSegments within the permitted set of templates. 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
// template segment 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).
// - 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.
//
// 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 that previous node is on an
// optimal path towards the end node. This process repeats until the start node is
// reached, painting templates along the way.
//
// Note that this algorithm makes a few (reasonable) assumptions about the shapes of
// templates, such as that they don't individually snake around too much. The actual
// tiles of a template 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 ??= Direction.FromCVec(Points[1] - Points[0]);
end.Direction ??= Direction.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<int>(size).Fill(OverDeviation);
var lowProgress = new Matrix<int>(size).Fill(InvalidProgress);
var highProgress = new Matrix<int>(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;
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<int> values)
{
Debug.Assert(values.Count > 0, "No values");
if (values.Count == 1)
return (values[0], values[0]);
if (IsLoop)
{
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<int>(8);
var highs = new List<int>(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();
for (var i = 0; i < 8; i++)
{
var offset = Direction.Spread8[i];
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,
Direction.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 Direction.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,
Direction.Spread8);
}
var pathStart = points[0];
var pathEnd = points[^1];
var orderedPermittedSegments = Segments.All.ToImmutableArray();
var permittedSegments = orderedPermittedSegments.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>>();
{
void RegisterSegmentType(string type)
{
if (segmentTypeToId.ContainsKey(type))
return;
var newId = segmentTypeToId.Count;
segmentTypeToId.Add(type, newId);
segmentsByStart.Add(new List<TilingSegment>());
segmentsByEnd.Add(new List<TilingSegment>());
costs.Add(new Matrix<int>(size).Fill(MaxCost));
}
foreach (var segment in orderedPermittedSegments)
{
var template = Segments.TemplatedTerrainInfo.SegmentsToTemplates[segment];
RegisterSegmentType(segment.Start);
RegisterSegmentType(segment.End);
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 totalTypeIds = segmentTypeToId.Count;
var priorities = new PriorityArray<int>(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);
}
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;
}
}
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;
}
// Satisfies all requirements.
return deviationAcc;
}
void UpdateFrom(CVec from, int fromTypeId, int fromCost)
{
foreach (var segment in segmentsByStart[fromTypeId])
{
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 (toCost < costs[toTypeId][to.X, to.Y])
{
costs[toTypeId][to.X, to.Y] = toCost;
SetPriorityAt(toTypeId, to, 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);
while (true)
{
var (fromTypeId, from, priority) = GetNextPriority();
if (priority == MaxCost || from == pathEnd)
break;
UpdateFrom(from, fromTypeId, costs[fromTypeId][from.X, from.Y]);
}
// Trace back and update tiles
var resultPoints = new List<CPos>
{
new(pathEnd.X + minPoint.X, pathEnd.Y + minPoint.Y)
};
(CVec From, int FromTypeId) TraceBackStep(CVec to, int toTypeId, int toCost)
{
var candidates = new List<TilingSegment>();
foreach (var segment in segmentsByEnd[toTypeId])
{
var from = to - segment.Moves;
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;
if (fromCost == costs[segment.StartTypeId][from.X, from.Y])
candidates.Add(segment);
}
Debug.Assert(candidates.Count >= 1, "TraceBack didn't find an original route");
var chosenSegment = candidates[random.Next(candidates.Count)];
var chosenFrom = to - chosenSegment.Moves;
PaintTemplate(Map, chosenFrom - chosenSegment.Offset + minPoint, chosenSegment.TemplateInfo);
// Skip end point as it is recorded in the previous template.
for (var i = chosenSegment.RelativePoints.Length - 2; i >= 0; i--)
{
var point = chosenFrom + chosenSegment.RelativePoints[i] + minPoint;
resultPoints.Add(point);
}
return (chosenFrom, chosenSegment.StartTypeId);
}
{
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;
(to, toTypeId) = TraceBackStep(to, toTypeId, bestCost);
// 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]);
}
// Traced back in reverse, so reverse the reversal.
resultPoints.Reverse();
return resultPoints.ToArray();
}
static void PaintTemplate(Map map, CPos at, TerrainTemplateInfo template)
{
if (template.PickAny)
throw new ArgumentException("PaintTemplate does not expect PickAny");
for (var y = 0; y < template.Size.Y; y++)
for (var x = 0; x < template.Size.X; x++)
{
var i = (byte)(y * template.Size.X + x);
if (template[i] == null)
continue;
var tile = new TerrainTile(template.Id, i);
var mpos = new CPos(at.X + x, at.Y + y).ToMPos(map);
if (map.Tiles.Contains(mpos))
map.Tiles[mpos] = tile;
}
}
/// <summary>
/// <para>
/// 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.
/// </para>
/// <para>
/// Returns the object being called on.
/// </para>
/// </summary>
public TilingPath InertiallyExtend(int extensionLength, int inertialRange)
{
Points = InertiallyExtendPathPoints(Points, extensionLength, inertialRange);
return this;
}
/// <summary>
/// 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.
/// </summary>
public static CPos[] InertiallyExtendPathPoints(CPos[] points, int extensionLength, int inertialRange)
{
if (points == null)
return null;
if (inertialRange > points.Length - 1)
inertialRange = points.Length - 1;
var sd = Direction.FromCVecNonDiagonal(points[inertialRange] - points[0]);
var ed = Direction.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] - Direction.ToCVec(sd) * (extensionLength - i);
Array.Copy(points, 0, newPoints, extensionLength, points.Length);
for (var i = 0; i < extensionLength; i++)
newPoints[extensionLength + points.Length + i] = points[^1] + Direction.ToCVec(ed) * (i + 1);
return newPoints;
}
/// <summary>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// Starts/ends which are corner-connected or already extend beyond the edge are unaltered.
/// </para>
/// <para>
/// Returns the object being called on.
/// </para>
/// </summary>
public TilingPath ExtendEdge(int extensionLength)
{
Points = ExtendEdgePathPoints(Points, CellLayerUtils.CellBounds(Map), extensionLength);
return this;
}
/// <summary>
/// <para>
/// 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.
/// </para>
/// <para>
/// Starts/ends which are corner-connected or already extend beyond the edge are unaltered.
/// </para>
/// </summary>
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 Array.Empty<CPos>();
}
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;
}
/// <summary>
/// <para>
/// For loops, points are rotated such that the start/end reside in the longest straight.
/// For non-loops, the input points are returned unaltered.
/// </para>
/// <para>
/// Returns the object being called on.
/// </para>
/// </summary>
public TilingPath OptimizeLoop()
{
Points = OptimizeLoopPathPoints(Points);
return this;
}
/// <summary>
/// For loops, points are rotated such that the start/end reside in the longest straight.
/// For non-loops, the input points are returned unaltered.
/// </summary>
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;
}
}
/// <summary>
/// <para>
/// 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.
/// </para>
/// <para>
/// If a loop is provided, the path is not shrunk, but the minimumLength requirement still
/// holds.
/// </para>
/// <para>
/// Returns the object being called on.
/// </para>
/// </summary>
public TilingPath Shrink(int shrinkBy, int minimumLength)
{
Points = ShrinkPathPoints(Points, shrinkBy, minimumLength);
return this;
}
/// <summary>
/// <para>
/// Shrink a path by a given amount at both ends. If the number of points in the path drops
/// below minimumLength, null is returned.
/// </para>
/// <para>
/// If a loop is provided, the path is not shrunk, but the minimumLength requirement still
/// holds.
/// </para>
/// </summary>
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)];
}
/// <summary>
/// <para>
/// Takes a path and normalizes its progression direction around the map center.
/// Normalized but opposing paths rotate around the center in the same direction.
/// </para>
/// <para>
/// The measureFromCenter function must convert CVec positions to WVec offsets from the map
/// center.
/// </para>
/// </summary>
public TilingPath ChirallyNormalize(Func<CPos, WVec> measureFromCenter)
{
Points = ChirallyNormalizePathPoints(Points, measureFromCenter);
return this;
}
/// <summary>
/// <para>
/// Takes a path and normalizes its progression direction around the map center.
/// Normalized but opposing paths rotate around the center in the same direction.
/// </para>
/// <para>
/// The measureFromCenter function must convert CVec positions to WVec offsets from the map
/// center.
/// </para>
/// </summary>
public static CPos[] ChirallyNormalizePathPoints(CPos[] points, Func<CPos, WVec> 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 loop
start = points[1];
end = points[^2];
}
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;
}
/// <summary>
/// <para>
/// Retains paths which have no points in common with earlier (previous and retained) paths
/// from the input.
/// </para>
/// <para>
/// The underlying point sequences are NOT cloned.
/// </para>
/// <para>
/// All input sequences must be non-null.
/// </para>
/// </summary>
public static CPos[][] RetainDisjointPaths(IEnumerable<CPos[]> inputs)
{
var outputs = new List<CPos[]>();
var lookup = new HashSet<CPos>();
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();
}
}
}