baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View 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;
}
}

View 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]);
}
}

View 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);
}
}

View 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;
}
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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();
}

View 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();
}
}

View 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>

View 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));
}
}

View 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));
}
}

View 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
View 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; }
}

View 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;
}

View 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;
}
}

View 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;
}
}

View 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
View 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}";
}
}

View 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));
}
}

View 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);
}

View 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);
}
}
}

View 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);

View 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);
}
}

View 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);

View 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;
}
}

View 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));
}
}
}
}
}

View 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
View 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).

View 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;
}
}