baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
140
src/Pathfinding/BaseGridNetwork.cs
Normal file
140
src/Pathfinding/BaseGridNetwork.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
// <copyright file="BaseGridNetwork.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for grid networks.
|
||||
/// </summary>
|
||||
public abstract class BaseGridNetwork : INetwork
|
||||
{
|
||||
/// <summary>
|
||||
/// The grid node value of an unreachable grid coordinate.
|
||||
/// </summary>
|
||||
private const byte UnreachableGridNodeValue = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The bit flag which marks a safezone node.
|
||||
/// </summary>
|
||||
private const byte SafezoneBitFlag = 0b1000_0000;
|
||||
|
||||
/// <summary>
|
||||
/// The bit mask for the cost of a node.
|
||||
/// </summary>
|
||||
private const byte CostBitMask = 0b0111_1111;
|
||||
|
||||
private static readonly sbyte[,] DirectionOffsets =
|
||||
{
|
||||
{ 0, -1 },
|
||||
{ 1, 0 },
|
||||
{ 0, 1 },
|
||||
{ -1, 0 },
|
||||
{ 1, -1 },
|
||||
{ 1, 1 },
|
||||
{ -1, 1 },
|
||||
{ -1, -1 },
|
||||
};
|
||||
|
||||
private readonly int _numberOfDirections;
|
||||
private ushort _gridWidth;
|
||||
private ushort _gridHeight;
|
||||
|
||||
/// <summary>
|
||||
/// The two-dimensional grid.
|
||||
/// For each coordinate it contains the cost of traveling to it from a neighbor coordinate.
|
||||
/// The value of 0 means, that the coordinate is unreachable.
|
||||
/// </summary>
|
||||
private byte[,]? _grid;
|
||||
|
||||
/// <summary>
|
||||
/// A flag, if safezone nodes should be included in the network.
|
||||
/// </summary>
|
||||
private bool _includeSafezone;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseGridNetwork"/> class.
|
||||
/// </summary>
|
||||
/// <param name="allowDiagonals">If set to <c>true</c>, diagonal traveling is allowed.</param>
|
||||
protected BaseGridNetwork(bool allowDiagonals)
|
||||
{
|
||||
this._numberOfDirections = allowDiagonals ? 8 : 4;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual bool Prepare(Point start, Point end, byte[,] grid, bool includeSafezone)
|
||||
{
|
||||
this._grid = grid;
|
||||
this._gridWidth = (ushort)(grid.GetUpperBound(0) + 1);
|
||||
this._gridHeight = (ushort)(grid.GetUpperBound(1) + 1);
|
||||
this._includeSafezone = includeSafezone;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// Not sure, if the implementation should really filter out nodes based on their status and cost.
|
||||
/// </remarks>
|
||||
public IEnumerable<Node> GetPossibleNextNodes(Node node)
|
||||
{
|
||||
var grid = this._grid ?? throw new InvalidOperationException("Call Prepare before");
|
||||
|
||||
// ReSharper disable once TooWideLocalVariableScope performance improvement
|
||||
byte newX;
|
||||
|
||||
// ReSharper disable once TooWideLocalVariableScope performance improvement
|
||||
byte newY;
|
||||
|
||||
for (int i = 0; i < this._numberOfDirections; i++)
|
||||
{
|
||||
newX = (byte)(node.X + DirectionOffsets[i, 0]);
|
||||
newY = (byte)(node.Y + DirectionOffsets[i, 1]);
|
||||
|
||||
if (!this._includeSafezone && (grid[newX, newY] & SafezoneBitFlag) > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var costToNode = grid[newX, newY] & CostBitMask;
|
||||
if (!this.IsWithinBounds(newX, newY) || costToNode == UnreachableGridNodeValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var newPoint = new Point(newX, newY);
|
||||
var newNode = this.GetNodeAt(newPoint);
|
||||
if (newNode is null || newNode.Status == NodeStatus.Closed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var newG = node.CostUntilNow + this._grid[newNode.X, newNode.Y];
|
||||
|
||||
if (newNode.Status == NodeStatus.Open && newNode.CostUntilNow <= newG)
|
||||
{
|
||||
// The current node has less cost than the previous? then skip this node
|
||||
continue;
|
||||
}
|
||||
|
||||
newNode.CostUntilNow = newG;
|
||||
|
||||
yield return newNode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Node? GetNodeAt(Point position);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the coordinates are within bounds of this network.
|
||||
/// </summary>
|
||||
/// <param name="x">The x.</param>
|
||||
/// <param name="y">The y.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the coordinates are within bounds of this network; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
protected virtual bool IsWithinBounds(byte x, byte y)
|
||||
{
|
||||
return x < this._gridWidth && y < this._gridHeight;
|
||||
}
|
||||
}
|
||||
165
src/Pathfinding/BinaryMinHeap{T}.cs
Normal file
165
src/Pathfinding/BinaryMinHeap{T}.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
// <copyright file="BinaryMinHeap{T}.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
/// <summary>
|
||||
/// A binary min heap which implements <see cref="IPriorityQueue{T}"/>.
|
||||
/// Objects with the lowest index values appear at the top of the heap, and will be retrieved when calling <see cref="Pop"/>.
|
||||
/// Please note: This class is not thread safe! Push/Pop should not be executed by two different threads at the same time!.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class contains some optimizations which do not make the code nicer. However,
|
||||
/// this data structure is THE bottleneck of the pathfinding algorithm, so optimization is worth it.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type which should be contained in the heap.</typeparam>
|
||||
public class BinaryMinHeap<T> : IPriorityQueue<T>
|
||||
{
|
||||
private readonly List<T> _innerList = new();
|
||||
private readonly IComparer<T> _elementComparer;
|
||||
|
||||
/// <summary>Reused variable to reduce stack allocations.</summary>
|
||||
private int _i;
|
||||
|
||||
/// <summary>Reused variable to reduce stack allocations.</summary>
|
||||
private int _parentIndex;
|
||||
|
||||
/// <summary>Reused variable to reduce stack allocations.</summary>
|
||||
private int _left;
|
||||
|
||||
/// <summary>Reused variable to reduce stack allocations.</summary>
|
||||
private int _right;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BinaryMinHeap{T}"/> class.
|
||||
/// </summary>
|
||||
public BinaryMinHeap()
|
||||
{
|
||||
this._elementComparer = Comparer<T>.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BinaryMinHeap{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="comparer">The comparer.</param>
|
||||
public BinaryMinHeap(IComparer<T> comparer)
|
||||
{
|
||||
this._elementComparer = comparer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BinaryMinHeap{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="comparer">The comparer.</param>
|
||||
/// <param name="capacity">The capacity.</param>
|
||||
public BinaryMinHeap(IComparer<T> comparer, int capacity)
|
||||
{
|
||||
this._elementComparer = comparer;
|
||||
this._innerList.Capacity = capacity;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Count => this._innerList.Count;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Push(T item)
|
||||
{
|
||||
this._i = this._innerList.Count;
|
||||
this._innerList.Add(item);
|
||||
do
|
||||
{
|
||||
if (this._i == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
this._parentIndex = unchecked(this._i - 1) >> 1;
|
||||
if (this.OnCompareWithElementOfI(this._parentIndex) < 0)
|
||||
{
|
||||
this.SwitchElementsParentWithI();
|
||||
this._i = this._parentIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (true);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T Pop()
|
||||
{
|
||||
if (this.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Heap is empty");
|
||||
}
|
||||
|
||||
var result = this._innerList[0];
|
||||
this._i = 0;
|
||||
this._innerList[0] = this._innerList[^1];
|
||||
this._innerList.RemoveAt(this._innerList.Count - 1);
|
||||
do
|
||||
{
|
||||
this._parentIndex = this._i;
|
||||
this._left = unchecked((this._i << 1) + 1);
|
||||
this._right = unchecked((this._i << 1) + 2);
|
||||
if (this._innerList.Count > this._left && this.OnCompareWithElementOfI(this._left) > 0)
|
||||
{
|
||||
this._i = this._left;
|
||||
}
|
||||
|
||||
if (this._innerList.Count > this._right && this.OnCompareWithElementOfI(this._right) > 0)
|
||||
{
|
||||
this._i = this._right;
|
||||
}
|
||||
|
||||
if (this._i == this._parentIndex)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
this.SwitchElementsParentWithI();
|
||||
}
|
||||
while (true);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the smallest object without removing it.
|
||||
/// </summary>
|
||||
/// <returns>The smallest object.</returns>
|
||||
public T Peek()
|
||||
{
|
||||
if (this._innerList.Count > 0)
|
||||
{
|
||||
return this._innerList[0];
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Heap is empty");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Clear()
|
||||
{
|
||||
this._innerList.Clear();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void SwitchElementsParentWithI()
|
||||
{
|
||||
T h = this._innerList[this._i];
|
||||
this._innerList[this._i] = this._innerList[this._parentIndex];
|
||||
this._innerList[this._parentIndex] = h;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private int OnCompareWithElementOfI(int j)
|
||||
{
|
||||
return this._elementComparer.Compare(this._innerList[this._i], this._innerList[j]);
|
||||
}
|
||||
}
|
||||
21
src/Pathfinding/EuclideanHeuristic.cs
Normal file
21
src/Pathfinding/EuclideanHeuristic.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// <copyright file="EuclideanHeuristic.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// A heuristic which takes the maximum distance value between the x-axis or the y-axis.
|
||||
/// </summary>
|
||||
internal class EuclideanHeuristic : IHeuristic
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public int HeuristicEstimateMultiplier { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CalculateHeuristicDistance(Point location, Point target)
|
||||
{
|
||||
var distance = location.EuclideanDistanceTo(target);
|
||||
return (int)(this.HeuristicEstimateMultiplier * distance);
|
||||
}
|
||||
}
|
||||
55
src/Pathfinding/FullGridNetwork.cs
Normal file
55
src/Pathfinding/FullGridNetwork.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
// <copyright file="FullGridNetwork.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Network which is built of a two-dimensional grid of nodes where
|
||||
/// each coordinate has a fixed cost to reach it from any direction.
|
||||
/// The network provides the nodes of the whole grid.
|
||||
/// </summary>
|
||||
public class FullGridNetwork : BaseGridNetwork
|
||||
{
|
||||
private readonly Node[] _nodes;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FullGridNetwork"/> class.
|
||||
/// </summary>
|
||||
/// <param name="allowDiagonals">If set to <c>true</c>, diagonal traveling is allowed.</param>
|
||||
public FullGridNetwork(bool allowDiagonals)
|
||||
: base(allowDiagonals)
|
||||
{
|
||||
this._nodes = new Node[0x10000];
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Node GetNodeAt(Point position)
|
||||
{
|
||||
var nodeIndex = this.GetIndexOfPoint(position);
|
||||
var node = this._nodes[nodeIndex];
|
||||
if (node is null)
|
||||
{
|
||||
node = new Node { Position = position };
|
||||
this._nodes[nodeIndex] = node;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Prepare(Point start, Point end, byte[,] grid, bool includeSafezone)
|
||||
{
|
||||
foreach (var node in this._nodes.Where(n => n != null))
|
||||
{
|
||||
node.Status = NodeStatus.Undefined;
|
||||
}
|
||||
|
||||
return base.Prepare(start, end, grid, includeSafezone);
|
||||
}
|
||||
|
||||
private int GetIndexOfPoint(Point position)
|
||||
{
|
||||
return (position.Y << 8) + position.X;
|
||||
}
|
||||
}
|
||||
25
src/Pathfinding/IHeuristic.cs
Normal file
25
src/Pathfinding/IHeuristic.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// <copyright file="IHeuristic.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a heuristic which detemines a heuristic distance from the current location to the target location.
|
||||
/// As better the heuristic as faster an optimal path can be found.
|
||||
/// </summary>
|
||||
public interface IHeuristic
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the heuristic estimate multiply value.
|
||||
/// </summary>
|
||||
int HeuristicEstimateMultiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the heuristic distance from the location to the target.
|
||||
/// </summary>
|
||||
/// <param name="location">The location position.</param>
|
||||
/// <param name="target">The target position.</param>
|
||||
/// <returns>The heuristic distance from the location to the target.</returns>
|
||||
int CalculateHeuristicDistance(Point location, Point target);
|
||||
}
|
||||
19
src/Pathfinding/IIndexer{T}.cs
Normal file
19
src/Pathfinding/IIndexer{T}.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
// <copyright file="IIndexer{T}.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Describes an indexer for a <see cref="IndexedLinkedList{T}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type for which the indexer can calculate the index value.</typeparam>
|
||||
internal interface IIndexer<in T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the index value of the item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>The index value.</returns>
|
||||
int GetIndexValue(T item);
|
||||
}
|
||||
44
src/Pathfinding/INetwork.cs
Normal file
44
src/Pathfinding/INetwork.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// <copyright file="INetwork.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Interface of a network which can be used by the <see cref="PathFinder"/>.
|
||||
/// </summary>
|
||||
public interface INetwork
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the node at the specified position.
|
||||
/// </summary>
|
||||
/// <param name="position">The position.</param>
|
||||
/// <returns>The node at the specified position, or null if there is no node at this position.</returns>
|
||||
Node? GetNodeAt(Point position);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the nodes which can be reached by the specified node.
|
||||
/// </summary>
|
||||
/// <param name="node">The node.</param>
|
||||
/// <returns>The nodes which can be reached by the specified node.</returns>
|
||||
IEnumerable<Node> GetPossibleNextNodes(Node node);
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the network for the next path finding.
|
||||
/// Resets the status of all nodes of the network.
|
||||
/// Needed to be called before any new path is being searched.
|
||||
/// </summary>
|
||||
/// <param name="start">The start point.</param>
|
||||
/// <param name="end">The end point.</param>
|
||||
/// <param name="grid">
|
||||
/// The two-dimensional grid.
|
||||
/// For each coordinate it contains the cost of traveling to it from a neighbor coordinate.
|
||||
/// The value of 0 means, that the coordinate is unreachable, <see cref="BaseGridNetwork.UnreachableGridNodeValue" />.
|
||||
/// If the highest bit of a value is set, it means it's a coordinate of a safezone.
|
||||
/// </param>
|
||||
/// <param name="includeSafezone">If set to <c>true</c>, safezone nodes should be included in the search.</param>
|
||||
/// <returns>
|
||||
/// If the preparations were successful and the pathfinding can proceed.
|
||||
/// </returns>
|
||||
bool Prepare(Point start, Point end, byte[,] grid, bool includeSafezone);
|
||||
}
|
||||
29
src/Pathfinding/IPathFinder.cs
Normal file
29
src/Pathfinding/IPathFinder.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// <copyright file="IPathFinder.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
using System.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a path finder.
|
||||
/// </summary>
|
||||
internal interface IPathFinder
|
||||
{
|
||||
/// <summary>
|
||||
/// Finds the path between two points.
|
||||
/// </summary>
|
||||
/// <param name="start">The start point.</param>
|
||||
/// <param name="end">The end point.</param>
|
||||
/// <param name="terrain">
|
||||
/// The two-dimensional grid of the terrain.
|
||||
/// For each coordinate it contains the cost of traveling to it from a neighbor coordinate.
|
||||
/// The value of 0 means, that the coordinate is unreachable, <see cref="BaseGridNetwork.UnreachableGridNodeValue" />.
|
||||
/// If the highest bit of a value is set, it means it's a coordinate of a safezone.
|
||||
/// </param>
|
||||
/// <param name="includeSafezone">If set to <c>true</c>, safezone nodes should be included in the search.</param>
|
||||
/// <param name="cancellationToken">The optional cancellation token to cancel the operation.</param>
|
||||
/// <returns>The path between start and end, including <paramref name="end"/>, but excluding <paramref name="start"/>.</returns>
|
||||
IList<PathResultNode>? FindPath(Point start, Point end, byte[,] terrain, bool includeSafezone, CancellationToken cancellationToken = default);
|
||||
}
|
||||
40
src/Pathfinding/IPriorityQueue{T}.cs
Normal file
40
src/Pathfinding/IPriorityQueue{T}.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
// <copyright file="IPriorityQueue{T}.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a priority queue.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type which should be contained in the queue.</typeparam>
|
||||
public interface IPriorityQueue<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the number of elements in the queue.
|
||||
/// </summary>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the specified item into the queue, and puts them at the right place, based on it's priority.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
void Push(T item);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the instance with the highest priority, and removes it from the queue.
|
||||
/// </summary>
|
||||
/// <returns>The instance with the highest priority.</returns>
|
||||
T Pop();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the instance with the highest priority, without removing it from the queue.
|
||||
/// </summary>
|
||||
/// <returns>The instance with the highest priority.</returns>
|
||||
T Peek();
|
||||
|
||||
/// <summary>
|
||||
/// Clears this instance.
|
||||
/// </summary>
|
||||
void Clear();
|
||||
}
|
||||
120
src/Pathfinding/IndexedLinkedList{T}.cs
Normal file
120
src/Pathfinding/IndexedLinkedList{T}.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
// <copyright file="IndexedLinkedList{T}.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// An indexed linked list which implements <see cref="IPriorityQueue{T}"/>.
|
||||
/// Objects with the lowest index values appears at the first place of the list, and will be retrieved when calling <see cref="Pop"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This list is a bit slower (~ 3x) at the <see cref="Push"/> operation,
|
||||
/// but a lot faster (10x) at the <see cref="Pop"/> operation than
|
||||
/// the <see cref="BinaryMinHeap{T}"/>.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type which should be contained in the heap.</typeparam>
|
||||
internal class IndexedLinkedList<T> : IPriorityQueue<T>
|
||||
{
|
||||
private readonly LinkedList<T> _innerList;
|
||||
private readonly IComparer<T> _comparer;
|
||||
private readonly IIndexer<T> _indexer;
|
||||
|
||||
/// <summary>
|
||||
/// The index helps to find a better starting point to insert new nodes.
|
||||
/// </summary>
|
||||
private readonly IDictionary<int, LinkedListNode<T>> _index;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IndexedLinkedList{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="comparer">A comparer for the type which should be conained in the heap. It is used to determine the correct position in the heap.</param>
|
||||
/// <param name="indexer">The indexer, which returns an index value which is used as key.</param>
|
||||
public IndexedLinkedList(IComparer<T> comparer, IIndexer<T> indexer)
|
||||
{
|
||||
this._innerList = new LinkedList<T>();
|
||||
this._comparer = comparer;
|
||||
this._indexer = indexer;
|
||||
this._index = new Dictionary<int, LinkedListNode<T>>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Count => this._innerList.Count;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Push(T item)
|
||||
{
|
||||
int indexValue = this._indexer.GetIndexValue(item);
|
||||
if (this._innerList.First is null)
|
||||
{
|
||||
var addedNode = this._innerList.AddFirst(item);
|
||||
this._index.Add(indexValue, addedNode);
|
||||
return;
|
||||
}
|
||||
|
||||
var inIndex = true;
|
||||
if (!this._index.TryGetValue(indexValue, out var node))
|
||||
{
|
||||
node = this._innerList.First;
|
||||
inIndex = false;
|
||||
}
|
||||
|
||||
while (node != null && this._comparer.Compare(item, node.Value) > 0)
|
||||
{
|
||||
node = node.Next;
|
||||
}
|
||||
|
||||
if (node != null)
|
||||
{
|
||||
if (inIndex)
|
||||
{
|
||||
this._innerList.AddAfter(node, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
var addedNode = this._innerList.AddAfter(node, item);
|
||||
this._index.Add(indexValue, addedNode);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var addedNode = this._innerList.AddLast(item);
|
||||
if (!inIndex)
|
||||
{
|
||||
this._index.Add(indexValue, addedNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T Pop()
|
||||
{
|
||||
if (this._innerList.First is { } first)
|
||||
{
|
||||
var value = first.Value;
|
||||
this._index.Remove(this._indexer.GetIndexValue(value)); // first value is probably always in the index...
|
||||
this._innerList.RemoveFirst();
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("List is empty");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T Peek()
|
||||
{
|
||||
if (this._innerList.First is { } first)
|
||||
{
|
||||
return first.Value;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("List is empty");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Clear()
|
||||
{
|
||||
this._innerList.Clear();
|
||||
this._index.Clear();
|
||||
}
|
||||
}
|
||||
20
src/Pathfinding/MUnique.OpenMU.Pathfinding.csproj
Normal file
20
src/Pathfinding/MUnique.OpenMU.Pathfinding.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>..\..\bin\Debug\</OutputPath>
|
||||
<DocumentationFile>..\..\bin\Debug\MUnique.OpenMU.Pathfinding.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>..\..\bin\Release\</OutputPath>
|
||||
<DocumentationFile>..\..\bin\Release\MUnique.OpenMU.Pathfinding.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
20
src/Pathfinding/ManhattanHeuristic.cs
Normal file
20
src/Pathfinding/ManhattanHeuristic.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// <copyright file="ManhattanHeuristic.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// The manhattan (because of how the buildings are placed) heuristic.
|
||||
/// </summary>
|
||||
internal class ManhattanHeuristic : IHeuristic
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public int HeuristicEstimateMultiplier { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CalculateHeuristicDistance(Point location, Point target)
|
||||
{
|
||||
return this.HeuristicEstimateMultiplier * (Math.Abs(location.X - target.X) + Math.Abs(location.Y - target.Y));
|
||||
}
|
||||
}
|
||||
20
src/Pathfinding/MaximumDistanceOfXorYHeuristic.cs
Normal file
20
src/Pathfinding/MaximumDistanceOfXorYHeuristic.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// <copyright file="MaximumDistanceOfXorYHeuristic.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// A heuristic which takes the maximum distance value between the x-axis or the y-axis.
|
||||
/// </summary>
|
||||
internal class MaximumDistanceOfXorYHeuristic : IHeuristic
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public int HeuristicEstimateMultiplier { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CalculateHeuristicDistance(Point location, Point target)
|
||||
{
|
||||
return this.HeuristicEstimateMultiplier * Math.Max(Math.Abs(location.X - target.X), Math.Abs(location.Y - target.Y));
|
||||
}
|
||||
}
|
||||
21
src/Pathfinding/NoHeuristic.cs
Normal file
21
src/Pathfinding/NoHeuristic.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// <copyright file="NoHeuristic.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// A heuristic which does not calculate a heuristic distance.
|
||||
/// </summary>
|
||||
/// <remarks>Using this equals the Dijkstra algorithm.</remarks>
|
||||
internal class NoHeuristic : IHeuristic
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public int HeuristicEstimateMultiplier { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CalculateHeuristicDistance(Point location, Point target)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
69
src/Pathfinding/Node.cs
Normal file
69
src/Pathfinding/Node.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
// <copyright file="Node.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// The status of a node.
|
||||
/// </summary>
|
||||
public enum NodeStatus : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// The status is undefined.
|
||||
/// </summary>
|
||||
Undefined,
|
||||
|
||||
/// <summary>
|
||||
/// The node is on the open list.
|
||||
/// </summary>
|
||||
Open,
|
||||
|
||||
/// <summary>
|
||||
/// The node is on the closed list.
|
||||
/// </summary>
|
||||
Closed,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A node of the path network.
|
||||
/// </summary>
|
||||
public class Node
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the predicted total cost (F) to reach the destination.
|
||||
/// </summary>
|
||||
/// <remarks>F = G + H.</remarks>
|
||||
public int PredictedTotalCost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cost which came up so far (G) to reach this node.
|
||||
/// </summary>
|
||||
/// <remarks>G.</remarks>
|
||||
public int CostUntilNow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the position of this node.
|
||||
/// </summary>
|
||||
public Point Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the x coordinate of this node.
|
||||
/// </summary>
|
||||
public byte X => this.Position.X;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the y coordinate of this node.
|
||||
/// </summary>
|
||||
public byte Y => this.Position.Y;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the previous node.
|
||||
/// </summary>
|
||||
public Node? PreviousNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the status.
|
||||
/// </summary>
|
||||
public NodeStatus Status { get; set; }
|
||||
}
|
||||
17
src/Pathfinding/NodeComparer.cs
Normal file
17
src/Pathfinding/NodeComparer.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
// <copyright file="NodeComparer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// A comparer to sort nodes based on their <see cref="Node.PredictedTotalCost"/> value.
|
||||
/// </summary>
|
||||
public class NodeComparer : IComparer<Node>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[SuppressMessage("ReSharper", "PossibleNullReferenceException", Justification = "We are sure that these parameters are never null. A check would reduce performance.")]
|
||||
public int Compare(Node? a, Node? b) => a!.PredictedTotalCost - b!.PredictedTotalCost;
|
||||
}
|
||||
19
src/Pathfinding/NodeIndexer.cs
Normal file
19
src/Pathfinding/NodeIndexer.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
// <copyright file="NodeIndexer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// An indexer for a <see cref="Node"/>, which can be used to calculate the index on the binary heap.
|
||||
/// The index is the predicted total cost to reach the node, divided by 10. So every 10 values,
|
||||
/// there is one index entry in the <see cref="IndexedLinkedList{T}._index"/>.
|
||||
/// </summary>
|
||||
internal class NodeIndexer : IIndexer<Node>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public int GetIndexValue(Node item)
|
||||
{
|
||||
return item.PredictedTotalCost / 10;
|
||||
}
|
||||
}
|
||||
220
src/Pathfinding/PathFinder.cs
Normal file
220
src/Pathfinding/PathFinder.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
// <copyright file="PathFinder.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// An implementation of the pathfinder which finds paths inside a two-dimensional grid.
|
||||
/// Please note, that this path finder is not thread safe,
|
||||
/// so only one search is allowed at the same time on one instance.
|
||||
/// </summary>
|
||||
public class PathFinder : IPathFinder
|
||||
{
|
||||
private static readonly Meter Meter = new(MeterName);
|
||||
private static readonly Counter<long> CurrentSearches = Meter.CreateCounter<long>("CurrentSearches");
|
||||
private static readonly Counter<long> CompletedSearches = Meter.CreateCounter<long>("CompletedSearches");
|
||||
private static readonly Counter<long> FailedSearches = Meter.CreateCounter<long>("FailedSearches");
|
||||
private static readonly Histogram<double> DurationCompletedMs = Meter.CreateHistogram<double>("DurationCompletedMs");
|
||||
private static readonly Histogram<double> DurationFailedMs = Meter.CreateHistogram<double>("DurationFailedMs");
|
||||
|
||||
private readonly INetwork _network;
|
||||
private readonly IPriorityQueue<Node> _openList;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathFinder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="network">The network on which the pathfinder should operate.</param>
|
||||
public PathFinder(INetwork network)
|
||||
: this(network, new BinaryMinHeap<Node>(new NodeComparer()))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathFinder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="network">The network on which the pathfinder should operate.</param>
|
||||
/// <param name="openList">The open list.</param>
|
||||
public PathFinder(INetwork network, IPriorityQueue<Node> openList)
|
||||
{
|
||||
this._network = network;
|
||||
this._openList = openList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the meter of this class.
|
||||
/// </summary>
|
||||
public static string MeterName => typeof(PathFinder).FullName ?? nameof(PathFinder);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum distance until which the path should be resolved.
|
||||
/// </summary>
|
||||
public int MaximumDistance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the search limit.
|
||||
/// </summary>
|
||||
public int SearchLimit { get; set; } = 500;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the heuristic estimate.
|
||||
/// </summary>
|
||||
public int HeuristicEstimate { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the heuristic.
|
||||
/// </summary>
|
||||
public IHeuristic Heuristic { get; set; } = new NoHeuristic();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IList<PathResultNode>? FindPath(Point start, Point end, byte[,] terrain, bool includeSafezone, CancellationToken cancellationToken = default)
|
||||
{
|
||||
CurrentSearches.Add(1);
|
||||
try
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var result = this.FindPathInner(start, end, terrain, includeSafezone, cancellationToken);
|
||||
var elapsedMs = (double)stopwatch.ElapsedTicks / TimeSpan.TicksPerMillisecond;
|
||||
if (result is null)
|
||||
{
|
||||
FailedSearches.Add(1);
|
||||
DurationFailedMs.Record(elapsedMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
CompletedSearches.Add(1);
|
||||
DurationCompletedMs.Record(elapsedMs);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
CurrentSearches.Add(-1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the pathfinder.
|
||||
/// </summary>
|
||||
public void ResetPathFinder()
|
||||
{
|
||||
this._openList.Clear();
|
||||
}
|
||||
|
||||
private IList<PathResultNode>? FindPathInner(Point start, Point end, byte[,] terrain, bool includeSafezone, CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.MaximumDistanceExceeded(start, end))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var pathFound = false;
|
||||
this._openList.Clear();
|
||||
if (!this._network.Prepare(start, end, terrain, includeSafezone))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.Heuristic != null)
|
||||
{
|
||||
this.Heuristic.HeuristicEstimateMultiplier = this.HeuristicEstimate;
|
||||
}
|
||||
|
||||
var closeNodeCounter = 0;
|
||||
var startNode = this._network.GetNodeAt(start);
|
||||
if (startNode is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
startNode.PredictedTotalCost = 2;
|
||||
startNode.PreviousNode = startNode;
|
||||
startNode.Status = NodeStatus.Open;
|
||||
this._openList.Push(startNode);
|
||||
while (this._openList.Count > 0 && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var node = this._openList.Pop();
|
||||
if (node.Status == NodeStatus.Closed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.X == end.X && node.Y == end.Y)
|
||||
{
|
||||
node.Status = NodeStatus.Closed;
|
||||
pathFound = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (closeNodeCounter > this.SearchLimit)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
this.ExpandNodes(node, start, end);
|
||||
node.Status = NodeStatus.Closed;
|
||||
closeNodeCounter++;
|
||||
}
|
||||
|
||||
if (pathFound)
|
||||
{
|
||||
return this.GetCalculatedPath(end).Reverse().ToList();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ExpandNodes(Node node, Point start, Point end)
|
||||
{
|
||||
foreach (var newNode in this._network.GetPossibleNextNodes(node))
|
||||
{
|
||||
if (this.MaximumDistanceExceeded(start, end, newNode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var heuristicEstimate = this.Heuristic?.CalculateHeuristicDistance(newNode.Position, end) ?? 0;
|
||||
newNode.PredictedTotalCost = newNode.CostUntilNow + heuristicEstimate;
|
||||
newNode.Status = NodeStatus.Open;
|
||||
newNode.PreviousNode = node;
|
||||
this._openList.Push(newNode);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<PathResultNode> GetCalculatedPath(Point end)
|
||||
{
|
||||
var node = this._network.GetNodeAt(end);
|
||||
while (node!.PreviousNode != node)
|
||||
{
|
||||
yield return new PathResultNode(node.Position, node.PreviousNode!.Position);
|
||||
node = node.PreviousNode;
|
||||
}
|
||||
}
|
||||
|
||||
private bool MaximumDistanceExceeded(Point start, Point end)
|
||||
{
|
||||
if (this.MaximumDistance != 0)
|
||||
{
|
||||
return start.EuclideanDistanceTo(end) > this.MaximumDistance;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool MaximumDistanceExceeded(Point start, Point end, Node node)
|
||||
{
|
||||
if (this.MaximumDistance != 0)
|
||||
{
|
||||
var distance = start.EuclideanDistanceTo(node.Position);
|
||||
distance += node.Position.EuclideanDistanceTo(end);
|
||||
return distance > this.MaximumDistance;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
23
src/Pathfinding/PathResultNode.cs
Normal file
23
src/Pathfinding/PathResultNode.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// <copyright file="PathResultNode.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// A path finder node.
|
||||
/// </summary>
|
||||
/// <param name="Point">The point.</param>
|
||||
/// <param name="PreviousPoint">The previous point.</param>
|
||||
public record struct PathResultNode(Point Point, Point PreviousPoint)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the x coordinate.
|
||||
/// </summary>
|
||||
public byte X => this.Point.X;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the y coordinate.
|
||||
/// </summary>
|
||||
public byte Y => this.Point.Y;
|
||||
}
|
||||
62
src/Pathfinding/Point.cs
Normal file
62
src/Pathfinding/Point.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
// <copyright file="Point.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a coordinate on a map.
|
||||
/// </summary>
|
||||
/// <param name="X">The x coordinate.</param>
|
||||
/// <param name="Y">The y coordinate.</param>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 2)]
|
||||
public record struct Point(byte X, byte Y)
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the Addition operator between two points.
|
||||
/// </summary>
|
||||
/// <param name="a">The first point.</param>
|
||||
/// <param name="b">The second point.</param>
|
||||
/// <returns>
|
||||
/// The result of the operator.
|
||||
/// </returns>
|
||||
public static Point operator +(Point a, Point b) => new((byte)(a.X + b.X), (byte)(a.Y + b.Y));
|
||||
|
||||
/// <summary>
|
||||
/// Implements the Subtraction operator between two points.
|
||||
/// </summary>
|
||||
/// <param name="a">The first point.</param>
|
||||
/// <param name="b">The second point.</param>
|
||||
/// <returns>
|
||||
/// The result of the operator.
|
||||
/// </returns>
|
||||
public static Point operator -(Point a, Point b) => new((byte)(a.X - b.X), (byte)(a.Y - b.Y));
|
||||
|
||||
/// <summary>
|
||||
/// Implements the Subtraction operator between a point and an integer.
|
||||
/// </summary>
|
||||
/// <param name="a">The first point.</param>
|
||||
/// <param name="d">The divisor.</param>
|
||||
/// <returns>
|
||||
/// The result of the operator.
|
||||
/// </returns>
|
||||
public static Point operator /(Point a, int d) => new((byte)(a.X / d), (byte)(a.Y / d));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the euclidean distance between this point and another point.
|
||||
/// </summary>
|
||||
/// <param name="otherPoint">The other point.</param>
|
||||
/// <returns>The distance between this point and another point.</returns>
|
||||
public double EuclideanDistanceTo(Point otherPoint)
|
||||
{
|
||||
return Math.Sqrt(Math.Pow(Math.Abs(this.X - otherPoint.X), 2) + Math.Pow(Math.Abs(this.Y - otherPoint.Y), 2));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.X}, {this.Y}";
|
||||
}
|
||||
}
|
||||
67
src/Pathfinding/PreCalculation/CompactPathsSerializer.cs
Normal file
67
src/Pathfinding/PreCalculation/CompactPathsSerializer.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
// <copyright file="CompactPathsSerializer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding.PreCalculation;
|
||||
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the path infos into a more compact format.
|
||||
/// This format uses just 1 byte for end and 1 byte for next step.
|
||||
/// It can only be used for maximumRange lower than 8, because of the space limitation.
|
||||
/// </summary>
|
||||
internal class CompactPathsSerializer : IPathsSerializer
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<PathInfo> Deserialize(Stream source)
|
||||
{
|
||||
const int elementSize = 6;
|
||||
|
||||
while (source.Position + elementSize < source.Length)
|
||||
{
|
||||
byte startX = (byte)source.ReadByte();
|
||||
byte startY = (byte)source.ReadByte();
|
||||
byte startEndDiff = (byte)source.ReadByte();
|
||||
byte startNextStepDiff = (byte)source.ReadByte();
|
||||
var start = new Point(startX, startY);
|
||||
byte xOffset = (byte)(startEndDiff >> 4 & 0x0F);
|
||||
byte yOffset = (byte)(startEndDiff & 0x0F);
|
||||
var end = new Point((byte)(startX + xOffset), (byte)(startY + yOffset));
|
||||
|
||||
byte xOffsetNext = (byte)(startNextStepDiff >> 4 & 0x0F);
|
||||
byte yOffsetNext = (byte)(startNextStepDiff & 0x0F);
|
||||
var nextStep = new Point((byte)(startX + xOffsetNext), (byte)(startY + yOffsetNext));
|
||||
yield return new PathInfo(new PointCombination(start, end), nextStep);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Serialize(IEnumerable<PathInfo> pathInfos, Stream targetStream)
|
||||
{
|
||||
foreach (var info in pathInfos)
|
||||
{
|
||||
targetStream.WriteByte(info.Combination.Start.X);
|
||||
targetStream.WriteByte(info.Combination.Start.Y);
|
||||
targetStream.WriteByte(CalcDiff(info.Combination.Start, info.Combination.End));
|
||||
targetStream.WriteByte(CalcDiff(info.Combination.Start, info.NextStep));
|
||||
}
|
||||
}
|
||||
|
||||
private static byte CalcDiff(Point start, Point end)
|
||||
{
|
||||
int diffX = end.X - start.X + 8;
|
||||
int diffY = end.Y - start.Y + 8;
|
||||
if (diffX > 15)
|
||||
{
|
||||
throw new ArgumentException($"The difference between start and end in the x value is greater than the allowed 15. start: {start}, end: {end}");
|
||||
}
|
||||
|
||||
if (diffY > 15)
|
||||
{
|
||||
throw new ArgumentException($"The difference between start and end in the y value is greater than the allowed 15. start: {start}, end: {end}");
|
||||
}
|
||||
|
||||
return (byte)(((diffX << 4) & 0xF0) | (diffY & 0x0F));
|
||||
}
|
||||
}
|
||||
27
src/Pathfinding/PreCalculation/IPathsSerializer.cs
Normal file
27
src/Pathfinding/PreCalculation/IPathsSerializer.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// <copyright file="IPathsSerializer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding.PreCalculation;
|
||||
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a paths serializer.
|
||||
/// </summary>
|
||||
internal interface IPathsSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Deserializes the path infos from the specified source.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <returns>The path infos.</returns>
|
||||
IEnumerable<PathInfo> Deserialize(Stream source);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the specified path infos into the stream.
|
||||
/// </summary>
|
||||
/// <param name="pathInfos">The path infos.</param>
|
||||
/// <param name="targetStream">The target stream.</param>
|
||||
void Serialize(IEnumerable<PathInfo> pathInfos, Stream targetStream);
|
||||
}
|
||||
41
src/Pathfinding/PreCalculation/NormalPathsSerializer.cs
Normal file
41
src/Pathfinding/PreCalculation/NormalPathsSerializer.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// <copyright file="NormalPathsSerializer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding.PreCalculation;
|
||||
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the path infos into the normal format. Every point uses exactly 2 bytes.
|
||||
/// </summary>
|
||||
/// <seealso cref="OpenMU.Pathfinding.PreCalculation.IPathsSerializer" />
|
||||
internal class NormalPathsSerializer : IPathsSerializer
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<PathInfo> Deserialize(Stream source)
|
||||
{
|
||||
const int elementSize = 8;
|
||||
while (source.Position + elementSize < source.Length)
|
||||
{
|
||||
var start = new Point((byte)source.ReadByte(), (byte)source.ReadByte());
|
||||
var end = new Point((byte)source.ReadByte(), (byte)source.ReadByte());
|
||||
var nextStep = new Point((byte)source.ReadByte(), (byte)source.ReadByte());
|
||||
yield return new PathInfo(new PointCombination(start, end), nextStep);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Serialize(IEnumerable<PathInfo> pathInfos, Stream targetStream)
|
||||
{
|
||||
foreach (var info in pathInfos)
|
||||
{
|
||||
targetStream.WriteByte(info.Combination.Start.X);
|
||||
targetStream.WriteByte(info.Combination.Start.Y);
|
||||
targetStream.WriteByte(info.Combination.End.X);
|
||||
targetStream.WriteByte(info.Combination.End.Y);
|
||||
targetStream.WriteByte(info.NextStep.X);
|
||||
targetStream.WriteByte(info.NextStep.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/Pathfinding/PreCalculation/PathInfo.cs
Normal file
15
src/Pathfinding/PreCalculation/PathInfo.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// <copyright file="PathInfo.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding.PreCalculation;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
/// Information about which is the <see cref="NextStep"/> to reach the <see cref="PointCombination.End"/> from the <see cref="PointCombination.Start"/>.
|
||||
/// </summary>
|
||||
/// <param name="Combination">The start/end point combination which acts like a key for the next step.</param>
|
||||
/// <param name="NextStep">The next step to get one step closer to the <see cref="PointCombination.End"/>.</param>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 6)]
|
||||
public record struct PathInfo(PointCombination Combination, Point NextStep);
|
||||
56
src/Pathfinding/PreCalculation/PathsSerializeExtensions.cs
Normal file
56
src/Pathfinding/PreCalculation/PathsSerializeExtensions.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
// <copyright file="PathsSerializeExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding.PreCalculation;
|
||||
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for paths serialization.
|
||||
/// </summary>
|
||||
public static class PathsSerializeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// The format in which the path infos will be serialized.
|
||||
/// </summary>
|
||||
public enum PathInfoFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// The compact format.
|
||||
/// This format uses just 1 byte for end and 1 byte for next step.
|
||||
/// It can only be used for maximumRange lower than 8, because of the space limitation.
|
||||
/// </summary>
|
||||
Compact,
|
||||
|
||||
/// <summary>
|
||||
/// The normal format. Every point uses exactly 2 bytes.
|
||||
/// </summary>
|
||||
Normal,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the path infos to the target stream.
|
||||
/// </summary>
|
||||
/// <param name="pathInfos">The path infos.</param>
|
||||
/// <param name="target">The target stream.</param>
|
||||
/// <param name="format">The format.</param>
|
||||
public static void SerializeToStream(this IEnumerable<PathInfo> pathInfos, Stream target, PathInfoFormat format)
|
||||
{
|
||||
IPathsSerializer serializer = format == PathInfoFormat.Compact ? new CompactPathsSerializer() as IPathsSerializer : new NormalPathsSerializer();
|
||||
target.WriteByte((byte)format);
|
||||
serializer.Serialize(pathInfos, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the path infos from the source stream.
|
||||
/// </summary>
|
||||
/// <param name="source">The source stream.</param>
|
||||
/// <returns>The path infos.</returns>
|
||||
public static IEnumerable<PathInfo> DeserializeFromStream(this Stream source)
|
||||
{
|
||||
var format = (PathInfoFormat)source.ReadByte();
|
||||
IPathsSerializer serializer = format == PathInfoFormat.Compact ? new CompactPathsSerializer() as IPathsSerializer : new NormalPathsSerializer();
|
||||
return serializer.Deserialize(source);
|
||||
}
|
||||
}
|
||||
15
src/Pathfinding/PreCalculation/PointCombination.cs
Normal file
15
src/Pathfinding/PreCalculation/PointCombination.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// <copyright file="PointCombination.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding.PreCalculation;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
/// A combination of the start and end point, which acts like a key for the next step to reach the end point.
|
||||
/// </summary>
|
||||
/// <param name="Start">The start point.</param>
|
||||
/// <param name="End">The end point.</param>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 4)]
|
||||
public record struct PointCombination(Point Start, Point End);
|
||||
43
src/Pathfinding/PreCalculation/PreCalculatedPathFinder.cs
Normal file
43
src/Pathfinding/PreCalculation/PreCalculatedPathFinder.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
// <copyright file="PreCalculatedPathFinder.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding.PreCalculation;
|
||||
|
||||
using System.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// Path finder which uses pre calculated paths of one specific map.
|
||||
/// </summary>
|
||||
public class PreCalculatedPathFinder : IPathFinder
|
||||
{
|
||||
private readonly IDictionary<PointCombination, Point> _nextSteps;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PreCalculatedPathFinder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="pathInfos">The path infos.</param>
|
||||
public PreCalculatedPathFinder(IEnumerable<PathInfo> pathInfos)
|
||||
{
|
||||
this._nextSteps = pathInfos.ToDictionary(info => info.Combination, info => info.NextStep);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IList<PathResultNode>? FindPath(Point start, Point end, byte[,] terrain, bool includeSafezone, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new List<PathResultNode>();
|
||||
Point nextStep;
|
||||
while (this._nextSteps.TryGetValue(new PointCombination(start, end), out nextStep))
|
||||
{
|
||||
result.Add(new PathResultNode(nextStep, start));
|
||||
start = nextStep;
|
||||
}
|
||||
|
||||
if (result.Count == 0 || nextStep != end)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
76
src/Pathfinding/PreCalculation/PreCalculator.cs
Normal file
76
src/Pathfinding/PreCalculation/PreCalculator.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
// <copyright file="PreCalculator.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding.PreCalculation;
|
||||
|
||||
using System.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// With this class you can pre-calculate all paths of a given map and save it in a compact way.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For more information, see http://www.gamedev.net/page/resources/_/technical/artificial-intelligence/precalculated-pathfinding-revisited-r1939.
|
||||
/// </remarks>
|
||||
public class PreCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Pres calcuates the paths from every possible point of the <paramref name="aiGrid" /> to any point of the <paramref name="aiGrid" /> in the <paramref name="maximumRange" />.
|
||||
/// </summary>
|
||||
/// <param name="aiGrid">The ai grid of the map, which includes the costs of moving to a specific coordinate.</param>
|
||||
/// <param name="walkMap">The grid of walkable coordinates of the map.</param>
|
||||
/// <param name="maximumRange">The maximum range.</param>
|
||||
/// <returns>All calculated path informations.</returns>
|
||||
public IEnumerable<PathInfo> PreCalcuatePaths(byte[,] aiGrid, bool[,] walkMap, int maximumRange)
|
||||
{
|
||||
var grid = aiGrid;
|
||||
int finished = 0;
|
||||
var resultList = new List<PathInfo>[256];
|
||||
Parallel.For(0, 256, new ParallelOptions { MaxDegreeOfParallelism = 4 }, (x) =>
|
||||
{
|
||||
var network = new FullGridNetwork(true);
|
||||
var pathFinder = new PathFinder(network);
|
||||
var result = new List<PathInfo>();
|
||||
resultList[x] = result;
|
||||
for (int y = 0; y < 256; y++)
|
||||
{
|
||||
if (!walkMap[x, y])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.AddRange(this.FindPaths(new Point((byte)x, (byte)y), walkMap, aiGrid, pathFinder, maximumRange));
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref finished);
|
||||
});
|
||||
|
||||
return resultList.SelectMany(pathInfo => pathInfo);
|
||||
}
|
||||
|
||||
private IEnumerable<PathInfo> FindPaths(Point start, bool[,] map, byte[,] aiGrid, IPathFinder pathFinder, int maxDistance)
|
||||
{
|
||||
byte toX = (byte)Math.Min(start.X + maxDistance - 1, 0xFF);
|
||||
byte toY = (byte)Math.Min(start.Y + maxDistance - 1, 0xFF);
|
||||
byte fromX = (byte)Math.Max(start.X - maxDistance, 0);
|
||||
byte fromY = (byte)Math.Max(start.Y - maxDistance, 0);
|
||||
for (byte x = fromX; x <= toX; x++)
|
||||
{
|
||||
for (byte y = fromY; y <= toY; y++)
|
||||
{
|
||||
if (!map[x, y] || (x == start.X && y == start.Y))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nodes = pathFinder.FindPath(new Point(x, y), start, aiGrid, false);
|
||||
if (nodes is { Count: > 0 })
|
||||
{
|
||||
var firstNode = nodes[0];
|
||||
|
||||
yield return new PathInfo(new PointCombination(new Point(start.X, start.Y), new Point(x, y)), new Point(firstNode.X, firstNode.Y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Pathfinding/Properties/AssemblyInfo.cs
Normal file
13
src/Pathfinding/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
// <copyright file="AssemblyInfo.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("MUnique.OpenMU.Pathfinding")]
|
||||
|
||||
[assembly: InternalsVisibleTo("MUnique.OpenMU.Pathfinding.Tests")]
|
||||
43
src/Pathfinding/Readme.md
Normal file
43
src/Pathfinding/Readme.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Pathfinding
|
||||
|
||||
This projects includes an a-star pathfinding algorithm, specifically for maps
|
||||
which have a maximum size of 256 x 256. This limitation arises by using byte
|
||||
fields for the coordinates, which saves some memory.
|
||||
There are a few heuristics available but by default the PathFinder uses NoHeuristic
|
||||
which is basically equal to using the Dijkstra algorithm.
|
||||
|
||||
## Priority Queue
|
||||
|
||||
There are two implementations of a priority queue of the Open-List: BinaryMinHeap
|
||||
and IndexedLinkedList.
|
||||
I suggest using the BinaryMinHeap because it's commonly used and it's hard to get
|
||||
IndexedLinkedList working faster under real circumstances.
|
||||
The reason is, that it's pretty hard to get the index fast under all conditions,
|
||||
because the expected open list lengths and estimated costs are always different.
|
||||
|
||||
## Scoped
|
||||
|
||||
The implementation can be used in a scoped way, which means that the pathfinder
|
||||
is only used for a scoped area of the map. This is useful if you want to calculate
|
||||
paths very quickly and you know that the path is only needed in a small area.
|
||||
You can read about that on my blog post: [Optimized Pathfinding](https://munique.net/optimizing-pathfinding/).
|
||||
|
||||
## Safezones
|
||||
|
||||
Safezones are areas on the map where usually no path should be calculated, except
|
||||
for special NPCs like guards. By default, the pathfinder will not calculate paths
|
||||
on the safezone tiles. You can change this behavior by passing the parameter
|
||||
`includeSafezone`. The safezones are encoded into the grid cost values as the
|
||||
highest bit.
|
||||
|
||||
## Pre-Calculation
|
||||
|
||||
In the sub-folder PreCalculation includes a pathfinder which makes use of
|
||||
pre-calculated paths. It's not used yet by OpenMU and needs some further testing.
|
||||
|
||||
For more information, visit <http://www.gamedev.net/page/resources/_/technical/artificial-intelligence/precalculated-pathfinding-revisited-r1939>.
|
||||
|
||||
## Further optimizations
|
||||
|
||||
If we find out that the current implementation is too slow, we could implement
|
||||
[Jump Point Search](http://www.gdcvault.com/play/1022094/JPS-Over-100x-Faster-than).
|
||||
131
src/Pathfinding/ScopedGridNetwork.cs
Normal file
131
src/Pathfinding/ScopedGridNetwork.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
// <copyright file="ScopedGridNetwork.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Network which is built of a two-dimensional grid of nodes where
|
||||
/// each coordinate has a fixed cost to reach it from any direction.
|
||||
/// The network proves the nodes of a smaller scope of the grid.
|
||||
/// </summary>
|
||||
public sealed class ScopedGridNetwork : BaseGridNetwork
|
||||
{
|
||||
private readonly Node?[] _gridNodes;
|
||||
private readonly byte _maximumSegmentSideLength;
|
||||
private readonly byte _minimumSegmentSideLength;
|
||||
private int _bitsPerCoordinate;
|
||||
private byte _actualSegmentSideLength;
|
||||
|
||||
/// <summary>
|
||||
/// The offset of the <see cref="_gridNodes"/> in which the calculation takes place.
|
||||
/// </summary>
|
||||
private Point _segmentOffset;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScopedGridNetwork" /> class.
|
||||
/// </summary>
|
||||
/// <param name="allowDiagonals">If set to <c>true</c>, diagonal traveling is allowed.</param>
|
||||
/// <param name="maximumSegmentSideLength">Maximum Length of the segment side. Should be a power of 2.</param>
|
||||
/// <param name="minimumSegmentSideLength">Minimum length of the segment side. Should be a power of 2.</param>
|
||||
public ScopedGridNetwork(bool allowDiagonals = true, byte maximumSegmentSideLength = 16, byte minimumSegmentSideLength = 8)
|
||||
: base(allowDiagonals)
|
||||
{
|
||||
this._gridNodes = new Node[maximumSegmentSideLength * maximumSegmentSideLength];
|
||||
this._maximumSegmentSideLength = maximumSegmentSideLength;
|
||||
this._minimumSegmentSideLength = minimumSegmentSideLength;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Node? GetNodeAt(Point position)
|
||||
{
|
||||
var nodeIndex = this.GetIndexOfPoint(position.X, position.Y);
|
||||
if (nodeIndex < 0 || nodeIndex >= this._actualSegmentSideLength * this._actualSegmentSideLength)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var node = this._gridNodes[nodeIndex];
|
||||
if (node is null)
|
||||
{
|
||||
node = new Node { Position = position };
|
||||
this._gridNodes[nodeIndex] = node;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Prepare(Point start, Point end, byte[,] grid, bool includeSafezone)
|
||||
{
|
||||
var diffX = Math.Abs(end.X - start.X);
|
||||
var diffY = Math.Abs(end.Y - start.Y);
|
||||
if (diffX > this._maximumSegmentSideLength || diffY > this._maximumSegmentSideLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this._actualSegmentSideLength = this._minimumSegmentSideLength;
|
||||
while ((diffX > this._actualSegmentSideLength - 1 || diffY > this._actualSegmentSideLength - 1)
|
||||
&& this._actualSegmentSideLength < this._maximumSegmentSideLength)
|
||||
{
|
||||
this._actualSegmentSideLength *= 2;
|
||||
}
|
||||
|
||||
this._bitsPerCoordinate = (int)Math.Log(this._actualSegmentSideLength, 2);
|
||||
var avg = (start / 2) + (end / 2);
|
||||
|
||||
var offsetX = GetOffset(avg.X, grid.GetUpperBound(0) + 1);
|
||||
var offsetY = GetOffset(avg.Y, grid.GetUpperBound(1) + 1);
|
||||
this._segmentOffset = new(offsetX, offsetY);
|
||||
|
||||
var maxX = offsetX + this._actualSegmentSideLength;
|
||||
var maxY = offsetY + this._actualSegmentSideLength;
|
||||
|
||||
for (byte x = offsetX; x < maxX; ++x)
|
||||
{
|
||||
for (byte y = offsetY; y < maxY; ++y)
|
||||
{
|
||||
var i = this.GetIndexOfPoint(x, y);
|
||||
var node = this._gridNodes[i];
|
||||
if (node is not null)
|
||||
{
|
||||
node.Status = NodeStatus.Undefined;
|
||||
node.Position = new(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return base.Prepare(start, end, grid, includeSafezone);
|
||||
|
||||
byte GetOffset(byte avgValue, int gridSize)
|
||||
{
|
||||
var offset = (byte)Math.Max(avgValue - (this._actualSegmentSideLength / 2), 0);
|
||||
offset = (byte)Math.Min(offset, gridSize - this._actualSegmentSideLength);
|
||||
return offset;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool IsWithinBounds(byte x, byte y)
|
||||
{
|
||||
return base.IsWithinBounds(x, y)
|
||||
&& x >= this._segmentOffset.X
|
||||
&& y >= this._segmentOffset.Y
|
||||
&& x < this._segmentOffset.X + this._actualSegmentSideLength
|
||||
&& y < this._segmentOffset.Y + this._actualSegmentSideLength;
|
||||
}
|
||||
|
||||
private int GetIndexOfPoint(int x, int y)
|
||||
{
|
||||
y -= this._segmentOffset.Y;
|
||||
x -= this._segmentOffset.X;
|
||||
|
||||
if (x < 0 || y < 0 || x >= this._actualSegmentSideLength || y >= this._actualSegmentSideLength)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (y << this._bitsPerCoordinate) + x;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user