NPC refactor (#10122)
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
This commit is contained in:
789
Content.Server/NPC/Pathfinding/Accessible/AiReachableSystem.cs
Normal file
789
Content.Server/NPC/Pathfinding/Accessible/AiReachableSystem.cs
Normal file
@@ -0,0 +1,789 @@
|
||||
using Content.Server.NPC.Pathfinding.Pathfinders;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.AI;
|
||||
using Content.Shared.GameTicking;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.NPC.Pathfinding.Accessible
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether an AI has access to a specific pathfinding node.
|
||||
/// </summary>
|
||||
/// Long-term can be used to do hierarchical pathfinding
|
||||
[UsedImplicitly]
|
||||
public sealed class AiReachableSystem : EntitySystem
|
||||
{
|
||||
/*
|
||||
* The purpose of this is to provide a higher-level / hierarchical abstraction of the actual pathfinding graph
|
||||
* The goal is so that we can more quickly discern if a specific node is reachable or not rather than
|
||||
* Pathfinding the entire graph.
|
||||
*
|
||||
* There's a lot of different implementations of hierarchical or some variation of it: HPA*, PRA, HAA*, etc.
|
||||
* (HPA* technically caches the edge nodes of each chunk), e.g. Rimworld, Factorio, etc.
|
||||
* so we'll just write one with SS14's requirements in mind.
|
||||
*
|
||||
* There's probably a better data structure to use though you'd need to benchmark multiple ones to compare,
|
||||
* at the very least on the memory side it could definitely be better.
|
||||
*/
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly PathfindingSystem _pathfindingSystem = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Queued region updates
|
||||
/// </summary>
|
||||
private readonly HashSet<PathfindingChunk> _queuedUpdates = new();
|
||||
|
||||
// Oh god the nesting. Shouldn't need to go beyond this
|
||||
/// <summary>
|
||||
/// The corresponding regions for each PathfindingChunk.
|
||||
/// Regions are groups of nodes with the same profile (for pathfinding purposes)
|
||||
/// i.e. same collision, not-space, same access, etc.
|
||||
/// </summary>
|
||||
private readonly Dictionary<EntityUid, Dictionary<PathfindingChunk, HashSet<PathfindingRegion>>> _regions =
|
||||
new();
|
||||
|
||||
/// <summary>
|
||||
/// Minimum time for the cached reachable regions to be stored
|
||||
/// </summary>
|
||||
private const float MinCacheTime = 1.0f;
|
||||
|
||||
// Cache what regions are accessible from this region. Cached per ReachableArgs
|
||||
// so multiple entities in the same region with the same args should all be able to share their reachable lookup
|
||||
// Also need to store when we cached it to know if it's stale if the chunks have updated
|
||||
|
||||
// TODO: There's probably a more memory-efficient way to cache this
|
||||
// Then again, there's likely also a more memory-efficient way to implement regions.
|
||||
|
||||
// Also, didn't use a dictionary because there didn't seem to be a clean way to do the lookup
|
||||
// Plus this way we can check if everything is equal except for vision so an entity with a lower vision radius can use an entity with a higher vision radius' cached result
|
||||
private readonly Dictionary<ReachableArgs, Dictionary<PathfindingRegion, (TimeSpan CacheTime, HashSet<PathfindingRegion> Regions)>> _cachedAccessible =
|
||||
new();
|
||||
|
||||
private readonly List<PathfindingRegion> _queuedCacheDeletions = new();
|
||||
|
||||
#if DEBUG
|
||||
private HashSet<IPlayerSession> _subscribedSessions = new();
|
||||
private int _runningCacheIdx = 0;
|
||||
#endif
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||
SubscribeLocalEvent<PathfindingChunkUpdateMessage>(RecalculateNodeRegions);
|
||||
SubscribeLocalEvent<GridRemovalEvent>(GridRemoved);
|
||||
#if DEBUG
|
||||
SubscribeNetworkEvent<SharedAiDebug.SubscribeReachableMessage>(HandleSubscription);
|
||||
SubscribeNetworkEvent<SharedAiDebug.UnsubscribeReachableMessage>(HandleUnsubscription);
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
_queuedUpdates.Clear();
|
||||
_regions.Clear();
|
||||
_cachedAccessible.Clear();
|
||||
_queuedCacheDeletions.Clear();
|
||||
|
||||
}
|
||||
|
||||
private void GridRemoved(GridRemovalEvent ev)
|
||||
{
|
||||
_regions.Remove(ev.EntityUid);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
foreach (var chunk in _queuedUpdates)
|
||||
{
|
||||
GenerateRegions(chunk);
|
||||
}
|
||||
|
||||
// TODO: Only send diffs instead
|
||||
#if DEBUG
|
||||
if (_subscribedSessions.Count > 0 && _queuedUpdates.Count > 0)
|
||||
{
|
||||
foreach (var (gridId, regs) in _regions)
|
||||
{
|
||||
if (regs.Count > 0)
|
||||
{
|
||||
SendRegionsDebugMessage(gridId);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
_queuedUpdates.Clear();
|
||||
|
||||
foreach (var region in _queuedCacheDeletions)
|
||||
{
|
||||
ClearCache(region);
|
||||
}
|
||||
|
||||
_queuedCacheDeletions.Clear();
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private void HandleSubscription(SharedAiDebug.SubscribeReachableMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
_subscribedSessions.Add((IPlayerSession) eventArgs.SenderSession);
|
||||
foreach (var (gridId, _) in _regions)
|
||||
{
|
||||
SendRegionsDebugMessage(gridId);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleUnsubscription(SharedAiDebug.UnsubscribeReachableMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
_subscribedSessions.Remove((IPlayerSession) eventArgs.SenderSession);
|
||||
}
|
||||
#endif
|
||||
|
||||
private void RecalculateNodeRegions(PathfindingChunkUpdateMessage message)
|
||||
{
|
||||
// TODO: Only need to do changed nodes ideally
|
||||
// For now this is fine but it's a low-hanging fruit optimisation
|
||||
_queuedUpdates.Add(message.Chunk);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can the entity reach the target?
|
||||
/// </summary>
|
||||
/// First it does a quick check to see if there are any traversable nodes in range.
|
||||
/// Then it will go through the regions to try and see if there's a region connection between the target and itself
|
||||
/// Will used a cached region if available
|
||||
/// <param name="entity"></param>
|
||||
/// <param name="target"></param>
|
||||
/// <param name="range"></param>
|
||||
/// <returns></returns>
|
||||
public bool CanAccess(EntityUid entity, EntityUid target, float range = 0.0f)
|
||||
{
|
||||
var xform = EntityManager.GetComponent<TransformComponent>(target);
|
||||
// TODO: Handle this gracefully instead of just failing.
|
||||
if (xform.GridUid == null)
|
||||
return false;
|
||||
|
||||
var targetTile = _mapManager.GetGrid(xform.GridUid.Value).GetTileRef(xform.Coordinates);
|
||||
var targetNode = _pathfindingSystem.GetNode(targetTile);
|
||||
|
||||
var collisionMask = 0;
|
||||
if (EntityManager.TryGetComponent(entity, out IPhysBody? physics))
|
||||
{
|
||||
collisionMask = physics.CollisionMask;
|
||||
}
|
||||
|
||||
var access = _accessReader.FindAccessTags(entity);
|
||||
|
||||
// We'll do a quick traversable check before going through regions
|
||||
// If we can't access it we'll try to get a valid node in range (this is essentially an early-out)
|
||||
if (!PathfindingHelpers.Traversable(collisionMask, access, targetNode))
|
||||
{
|
||||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
||||
if (range == 0.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var pathfindingArgs = new PathfindingArgs(entity, access, collisionMask, default, targetTile, range);
|
||||
foreach (var node in BFSPathfinder.GetNodesInRange(pathfindingArgs, false))
|
||||
{
|
||||
targetNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
return CanAccess(entity, targetNode);
|
||||
}
|
||||
|
||||
public bool CanAccess(EntityUid entity, PathfindingNode targetNode)
|
||||
{
|
||||
var xform = EntityManager.GetComponent<TransformComponent>(entity);
|
||||
|
||||
if (xform.GridUid != targetNode.TileRef.GridUid || xform.GridUid == null)
|
||||
return false;
|
||||
|
||||
var entityTile = _mapManager.GetGrid(xform.GridUid.Value).GetTileRef(xform.Coordinates);
|
||||
var entityNode = _pathfindingSystem.GetNode(entityTile);
|
||||
var entityRegion = GetRegion(entityNode);
|
||||
var targetRegion = GetRegion(targetNode);
|
||||
// TODO: Regional pathfind from target to entity
|
||||
// Early out
|
||||
if (entityRegion == targetRegion)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// We'll go from target's position to us because most of the time it's probably in a locked room rather than vice versa
|
||||
var reachableArgs = ReachableArgs.GetArgs(entity, 7f);
|
||||
var reachableRegions = GetReachableRegions(reachableArgs, targetRegion);
|
||||
|
||||
return entityRegion != null && reachableRegions.Contains(entityRegion);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the reachable regions
|
||||
/// </summary>
|
||||
/// <param name="reachableArgs"></param>
|
||||
/// <param name="region"></param>
|
||||
/// <returns></returns>
|
||||
public HashSet<PathfindingRegion> GetReachableRegions(ReachableArgs reachableArgs, PathfindingRegion? region)
|
||||
{
|
||||
// if we're on a node that's not tracked at all atm then region will be null
|
||||
if (region == null)
|
||||
{
|
||||
return new HashSet<PathfindingRegion>();
|
||||
}
|
||||
|
||||
var cachedArgs = GetCachedArgs(reachableArgs);
|
||||
(TimeSpan CacheTime, HashSet<PathfindingRegion> Regions) cached;
|
||||
|
||||
if (!IsCacheValid(cachedArgs, region))
|
||||
{
|
||||
cached = GetVisionReachable(cachedArgs, region);
|
||||
_cachedAccessible[cachedArgs][region] = cached;
|
||||
#if DEBUG
|
||||
SendRegionCacheMessage(region.ParentChunk.GridId, cached.Regions, false);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
cached = _cachedAccessible[cachedArgs][region];
|
||||
#if DEBUG
|
||||
SendRegionCacheMessage(region.ParentChunk.GridId, cached.Regions, true);
|
||||
#endif
|
||||
}
|
||||
|
||||
return cached.Regions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get any adequate cached args if possible, otherwise just use ours
|
||||
/// </summary>
|
||||
/// Essentially any args that have the same access AND >= our vision radius can be used
|
||||
/// <param name="accessibleArgs"></param>
|
||||
/// <returns></returns>
|
||||
private ReachableArgs GetCachedArgs(ReachableArgs accessibleArgs)
|
||||
{
|
||||
ReachableArgs? foundArgs = null;
|
||||
|
||||
foreach (var (cachedAccessible, _) in _cachedAccessible)
|
||||
{
|
||||
if (Equals(cachedAccessible.Access, accessibleArgs.Access) &&
|
||||
cachedAccessible.CollisionMask == accessibleArgs.CollisionMask &&
|
||||
cachedAccessible.VisionRadius <= accessibleArgs.VisionRadius)
|
||||
{
|
||||
foundArgs = cachedAccessible;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return foundArgs ?? accessibleArgs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether there's a valid cache for our accessibility args.
|
||||
/// Most regular mobs can share their cached accessibility with each other
|
||||
/// </summary>
|
||||
/// Will also remove it from the cache if it is invalid
|
||||
/// <param name="accessibleArgs"></param>
|
||||
/// <param name="region"></param>
|
||||
/// <returns></returns>
|
||||
private bool IsCacheValid(ReachableArgs accessibleArgs, PathfindingRegion region)
|
||||
{
|
||||
if (!_cachedAccessible.TryGetValue(accessibleArgs, out var cachedArgs))
|
||||
{
|
||||
_cachedAccessible.Add(accessibleArgs, new Dictionary<PathfindingRegion, (TimeSpan, HashSet<PathfindingRegion>)>());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!cachedArgs.TryGetValue(region, out var regionCache))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Just so we don't invalidate the cache every tick we'll store it for a minimum amount of time
|
||||
var currentTime = _gameTiming.CurTime;
|
||||
if ((currentTime - regionCache.CacheTime).TotalSeconds < MinCacheTime)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var checkedAccess = new HashSet<PathfindingRegion>();
|
||||
// Check if cache is stale
|
||||
foreach (var accessibleRegion in regionCache.Regions)
|
||||
{
|
||||
if (checkedAccess.Contains(accessibleRegion)) continue;
|
||||
|
||||
// Any applicable chunk has been invalidated OR one of our neighbors has been invalidated (i.e. new connections)
|
||||
// TODO: Could look at storing the TimeSpan directly on the region so our neighbor can tell us straight-up
|
||||
if (accessibleRegion.ParentChunk.LastUpdate > regionCache.CacheTime)
|
||||
{
|
||||
// Remove the stale cache, to be updated later
|
||||
_cachedAccessible[accessibleArgs].Remove(region);
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var neighbor in accessibleRegion.Neighbors)
|
||||
{
|
||||
if (checkedAccess.Contains(neighbor)) continue;
|
||||
if (neighbor.ParentChunk.LastUpdate > regionCache.CacheTime)
|
||||
{
|
||||
_cachedAccessible[accessibleArgs].Remove(region);
|
||||
return false;
|
||||
}
|
||||
checkedAccess.Add(neighbor);
|
||||
}
|
||||
checkedAccess.Add(accessibleRegion);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Caches the entity's nearby accessible regions in vision radius
|
||||
/// </summary>
|
||||
/// Longer-term TODO: Hierarchical pathfinding in which case this function would probably get bulldozed, BRRRTT
|
||||
/// <param name="reachableArgs"></param>
|
||||
/// <param name="entityRegion"></param>
|
||||
private (TimeSpan, HashSet<PathfindingRegion>) GetVisionReachable(ReachableArgs reachableArgs, PathfindingRegion entityRegion)
|
||||
{
|
||||
var openSet = new Queue<PathfindingRegion>();
|
||||
openSet.Enqueue(entityRegion);
|
||||
var closedSet = new HashSet<PathfindingRegion>();
|
||||
var accessible = new HashSet<PathfindingRegion> {entityRegion};
|
||||
|
||||
while (openSet.Count > 0)
|
||||
{
|
||||
var region = openSet.Dequeue();
|
||||
closedSet.Add(region);
|
||||
|
||||
foreach (var neighbor in region.Neighbors)
|
||||
{
|
||||
if (closedSet.Contains(neighbor))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Distance is an approximation here so we'll be generous with it
|
||||
// TODO: Could do better; the fewer nodes the better it is.
|
||||
if (!neighbor.RegionTraversable(reachableArgs) ||
|
||||
neighbor.Distance(entityRegion) > reachableArgs.VisionRadius + 1)
|
||||
{
|
||||
closedSet.Add(neighbor);
|
||||
continue;
|
||||
}
|
||||
|
||||
openSet.Enqueue(neighbor);
|
||||
accessible.Add(neighbor);
|
||||
}
|
||||
}
|
||||
|
||||
return (_gameTiming.CurTime, accessible);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grab the related cardinal nodes and if they're in different regions then add to our edge and their edge
|
||||
/// </summary>
|
||||
/// Implicitly they would've already been merged if possible
|
||||
/// <param name="region"></param>
|
||||
/// <param name="node"></param>
|
||||
private void UpdateRegionEdge(PathfindingRegion region, PathfindingNode node)
|
||||
{
|
||||
DebugTools.Assert(region.Nodes.Contains(node));
|
||||
// Originally I tried just doing bottom and left but that doesn't work as the chunk update order is not guaranteed
|
||||
|
||||
var checkDirections = new[] {Direction.East, Direction.South, Direction.West, Direction.North};
|
||||
foreach (var direction in checkDirections)
|
||||
{
|
||||
var directionNode = node.GetNeighbor(direction);
|
||||
if (directionNode == null) continue;
|
||||
|
||||
var directionRegion = GetRegion(directionNode);
|
||||
if (directionRegion == null || directionRegion == region) continue;
|
||||
|
||||
region.Neighbors.Add(directionRegion);
|
||||
directionRegion.Neighbors.Add(region);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current region for this entity
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <returns></returns>
|
||||
public PathfindingRegion? GetRegion(EntityUid entity)
|
||||
{
|
||||
var xform = EntityManager.GetComponent<TransformComponent>(entity);
|
||||
|
||||
if (xform.GridUid == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entityTile = _mapManager.GetGrid(xform.GridUid.Value).GetTileRef(xform.Coordinates);
|
||||
var entityNode = _pathfindingSystem.GetNode(entityTile);
|
||||
return GetRegion(entityNode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current region for this node
|
||||
/// </summary>
|
||||
/// <param name="node"></param>
|
||||
/// <returns></returns>
|
||||
public PathfindingRegion? GetRegion(PathfindingNode node)
|
||||
{
|
||||
// Not sure on the best way to optimise this
|
||||
// On the one hand, just storing each node's region is faster buuutttt muh memory
|
||||
// On the other hand, you might need O(n) lookups on regions for each chunk, though it's probably not too bad with smaller chunk sizes?
|
||||
// Someone smarter than me will know better
|
||||
var parentChunk = node.ParentChunk;
|
||||
|
||||
// No guarantee the node even has a region yet (if we're doing neighbor lookups)
|
||||
if (!_regions.TryGetValue(parentChunk.GridId, out var chunk))
|
||||
return null;
|
||||
|
||||
if (!chunk.TryGetValue(parentChunk, out var regions))
|
||||
return null;
|
||||
|
||||
foreach (var region in regions)
|
||||
{
|
||||
if (region.Nodes.Contains(node))
|
||||
{
|
||||
return region;
|
||||
}
|
||||
}
|
||||
|
||||
// Longer term this will probably be guaranteed a region but for now space etc. are no region
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add this node to the relevant region.
|
||||
/// </summary>
|
||||
/// <param name="node"></param>
|
||||
/// <param name="existingRegions">The cached region for each node</param>
|
||||
/// <param name="chunkRegions">The existing regions in the chunk</param>
|
||||
/// <param name="x">This is already calculated in advance so may as well re-use it</param>
|
||||
/// <param name="y">This is already calculated in advance so may as well re-use it</param>
|
||||
/// <returns></returns>
|
||||
private PathfindingRegion? CalculateNode(
|
||||
PathfindingNode node,
|
||||
Dictionary<PathfindingNode, PathfindingRegion> existingRegions,
|
||||
HashSet<PathfindingRegion> chunkRegions,
|
||||
int x, int y)
|
||||
{
|
||||
DebugTools.Assert(_regions.ContainsKey(node.ParentChunk.GridId));
|
||||
DebugTools.Assert(_regions[node.ParentChunk.GridId].ContainsKey(node.ParentChunk));
|
||||
// TODO For now we don't have these regions but longer-term yeah sure
|
||||
if (node.BlockedCollisionMask != 0x0 || node.TileRef.Tile.IsEmpty)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var parentChunk = node.ParentChunk;
|
||||
// Doors will be their own separate region
|
||||
// We won't store them in existingRegions so they don't show up and can't be connected to (at least for now)
|
||||
if (node.AccessReaders.Count > 0)
|
||||
{
|
||||
var region = new PathfindingRegion(node, new HashSet<PathfindingNode>(1) {node}, true);
|
||||
_regions[parentChunk.GridId][parentChunk].Add(region);
|
||||
UpdateRegionEdge(region, node);
|
||||
return region;
|
||||
}
|
||||
|
||||
// Relative x and y of the chunk
|
||||
// If one of our bottom / left neighbors are in a region try to join them
|
||||
// Otherwise, make our own region.
|
||||
var leftNeighbor = x > 0 ? parentChunk.Nodes[x - 1, y] : null;
|
||||
var bottomNeighbor = y > 0 ? parentChunk.Nodes[x, y - 1] : null;
|
||||
PathfindingRegion? leftRegion;
|
||||
PathfindingRegion? bottomRegion;
|
||||
|
||||
// We'll check if our left or down neighbors are already in a region and join them
|
||||
|
||||
// Is left node valid to connect to
|
||||
if (leftNeighbor != null &&
|
||||
existingRegions.TryGetValue(leftNeighbor, out leftRegion) &&
|
||||
!leftRegion.IsDoor)
|
||||
{
|
||||
// We'll try and connect the left node's region to the bottom region if they're separate (yay merge)
|
||||
if (bottomNeighbor != null &&
|
||||
existingRegions.TryGetValue(bottomNeighbor, out bottomRegion) &&
|
||||
bottomRegion != leftRegion &&
|
||||
!bottomRegion.IsDoor)
|
||||
{
|
||||
bottomRegion.Add(node);
|
||||
existingRegions.Add(node, bottomRegion);
|
||||
MergeInto(leftRegion, bottomRegion, existingRegions);
|
||||
|
||||
// Cleanup leftRegion
|
||||
// MergeInto will remove it from the overall region chunk cache while we need to remove it from
|
||||
// our short-term ones (chunkRegions and existingRegions)
|
||||
chunkRegions.Remove(leftRegion);
|
||||
|
||||
foreach (var leftNode in leftRegion.Nodes)
|
||||
{
|
||||
existingRegions[leftNode] = bottomRegion;
|
||||
}
|
||||
|
||||
return bottomRegion;
|
||||
}
|
||||
|
||||
leftRegion.Add(node);
|
||||
existingRegions.Add(node, leftRegion);
|
||||
UpdateRegionEdge(leftRegion, node);
|
||||
return leftRegion;
|
||||
}
|
||||
|
||||
//Is bottom node valid to connect to
|
||||
if (bottomNeighbor != null &&
|
||||
existingRegions.TryGetValue(bottomNeighbor, out bottomRegion) &&
|
||||
!bottomRegion.IsDoor)
|
||||
{
|
||||
bottomRegion.Add(node);
|
||||
existingRegions.Add(node, bottomRegion);
|
||||
UpdateRegionEdge(bottomRegion, node);
|
||||
return bottomRegion;
|
||||
}
|
||||
|
||||
// If we can't join an existing region then we'll make our own
|
||||
var newRegion = new PathfindingRegion(node, new HashSet<PathfindingNode> {node}, node.AccessReaders.Count > 0);
|
||||
_regions[parentChunk.GridId][parentChunk].Add(newRegion);
|
||||
existingRegions.Add(node, newRegion);
|
||||
UpdateRegionEdge(newRegion, node);
|
||||
return newRegion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines the two regions into one bigger region
|
||||
/// </summary>
|
||||
/// <param name="source"></param>
|
||||
/// <param name="target"></param>
|
||||
private void MergeInto(PathfindingRegion source, PathfindingRegion target, Dictionary<PathfindingNode, PathfindingRegion>? existingRegions = null)
|
||||
{
|
||||
DebugTools.AssertNotNull(source);
|
||||
DebugTools.AssertNotNull(target);
|
||||
DebugTools.Assert(source != target);
|
||||
foreach (var node in source.Nodes)
|
||||
{
|
||||
target.Add(node);
|
||||
}
|
||||
|
||||
if (existingRegions != null)
|
||||
{
|
||||
foreach (var node in source.Nodes)
|
||||
{
|
||||
existingRegions[node] = target;
|
||||
}
|
||||
}
|
||||
|
||||
source.Shutdown();
|
||||
// This doesn't check the cachedaccessible to see if it's reachable but maybe it should?
|
||||
// Although currently merge gets spammed so maybe when some other stuff is improved
|
||||
// MergeInto is also only called by GenerateRegions currently so nothing should hold onto the original region
|
||||
_regions[source.ParentChunk.GridId][source.ParentChunk].Remove(source);
|
||||
|
||||
foreach (var node in target.Nodes)
|
||||
{
|
||||
UpdateRegionEdge(target, node);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the cached accessibility lookup for this region
|
||||
/// </summary>
|
||||
/// <param name="region"></param>
|
||||
private void ClearCache(PathfindingRegion region)
|
||||
{
|
||||
DebugTools.Assert(region.Deleted);
|
||||
|
||||
// Need to forcibly clear cache for ourself and anything that includes us
|
||||
foreach (var (_, cachedRegions) in _cachedAccessible)
|
||||
{
|
||||
if (cachedRegions.ContainsKey(region))
|
||||
{
|
||||
cachedRegions.Remove(region);
|
||||
}
|
||||
|
||||
// Seemed like the safest way to remove this
|
||||
// We could just have GetVisionAccessible remove us if it can tell we're deleted but that
|
||||
// seems like it could be unreliable
|
||||
var regionsToClear = new List<PathfindingRegion>();
|
||||
|
||||
foreach (var (otherRegion, cache) in cachedRegions)
|
||||
{
|
||||
if (cache.Regions.Contains(region))
|
||||
{
|
||||
regionsToClear.Add(otherRegion);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var otherRegion in regionsToClear)
|
||||
{
|
||||
cachedRegions.Remove(otherRegion);
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if (_regions.TryGetValue(region.ParentChunk.GridId, out var chunks) &&
|
||||
chunks.TryGetValue(region.ParentChunk, out var regions))
|
||||
{
|
||||
DebugTools.Assert(!regions.Contains(region));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate all of the regions within a chunk
|
||||
/// </summary>
|
||||
/// These can't across over into another chunk and doors are their own region
|
||||
/// <param name="chunk"></param>
|
||||
private void GenerateRegions(PathfindingChunk chunk)
|
||||
{
|
||||
// Grid deleted while update queued, or invalid grid.
|
||||
if (!_mapManager.TryGetGrid(chunk.GridId, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_regions.ContainsKey(chunk.GridId))
|
||||
{
|
||||
_regions.Add(chunk.GridId, new Dictionary<PathfindingChunk, HashSet<PathfindingRegion>>());
|
||||
}
|
||||
|
||||
if (_regions[chunk.GridId].TryGetValue(chunk, out var regions))
|
||||
{
|
||||
foreach (var region in regions)
|
||||
{
|
||||
_queuedCacheDeletions.Add(region);
|
||||
region.Shutdown();
|
||||
}
|
||||
|
||||
_regions[chunk.GridId].Remove(chunk);
|
||||
}
|
||||
|
||||
// Temporarily store the corresponding region for each node
|
||||
// Makes merging regions or adding nodes to existing regions neater.
|
||||
var nodeRegions = new Dictionary<PathfindingNode, PathfindingRegion>();
|
||||
var chunkRegions = new HashSet<PathfindingRegion>();
|
||||
_regions[chunk.GridId].Add(chunk, chunkRegions);
|
||||
|
||||
for (var y = 0; y < PathfindingChunk.ChunkSize; y++)
|
||||
{
|
||||
for (var x = 0; x < PathfindingChunk.ChunkSize; x++)
|
||||
{
|
||||
var node = chunk.Nodes[x, y];
|
||||
var region = CalculateNode(node, nodeRegions, chunkRegions, x, y);
|
||||
// Currently we won't store a separate region for each mask / space / whatever because muh effort
|
||||
// Long-term you'll want to account for it probably
|
||||
if (region == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
chunkRegions.Add(region);
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
foreach (var region in chunkRegions)
|
||||
{
|
||||
DebugTools.Assert(!region.Deleted);
|
||||
}
|
||||
|
||||
DebugTools.Assert(chunkRegions.Count < Math.Pow(PathfindingChunk.ChunkSize, 2));
|
||||
SendRegionsDebugMessage(chunk.GridId);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Reset(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
_queuedUpdates.Clear();
|
||||
_regions.Clear();
|
||||
_cachedAccessible.Clear();
|
||||
_queuedCacheDeletions.Clear();
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private void SendRegionsDebugMessage(EntityUid gridId)
|
||||
{
|
||||
if (_subscribedSessions.Count == 0) return;
|
||||
var grid = _mapManager.GetGrid(gridId);
|
||||
// Chunk / Regions / Nodes
|
||||
var debugResult = new Dictionary<int, Dictionary<int, List<Vector2>>>();
|
||||
var chunkIdx = 0;
|
||||
var regionIdx = 0;
|
||||
|
||||
if (!_regions.TryGetValue(gridId, out var dict))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (_, regions) in dict)
|
||||
{
|
||||
var debugRegions = new Dictionary<int, List<Vector2>>();
|
||||
debugResult.Add(chunkIdx, debugRegions);
|
||||
|
||||
foreach (var region in regions)
|
||||
{
|
||||
var debugRegionNodes = new List<Vector2>(region.Nodes.Count);
|
||||
debugResult[chunkIdx].Add(regionIdx, debugRegionNodes);
|
||||
|
||||
foreach (var node in region.Nodes)
|
||||
{
|
||||
var nodeVector = grid.GridTileToLocal(node.TileRef.GridIndices).ToMapPos(EntityManager);
|
||||
debugRegionNodes.Add(nodeVector);
|
||||
}
|
||||
|
||||
regionIdx++;
|
||||
}
|
||||
|
||||
chunkIdx++;
|
||||
}
|
||||
|
||||
foreach (var session in _subscribedSessions)
|
||||
{
|
||||
RaiseNetworkEvent(new SharedAiDebug.ReachableChunkRegionsDebugMessage(gridId, debugResult), session.ConnectedClient);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent whenever the reachable cache for a particular mob is built or retrieved
|
||||
/// </summary>
|
||||
/// <param name="gridId"></param>
|
||||
/// <param name="regions"></param>
|
||||
/// <param name="cached"></param>
|
||||
private void SendRegionCacheMessage(EntityUid gridId, IEnumerable<PathfindingRegion> regions, bool cached)
|
||||
{
|
||||
if (_subscribedSessions.Count == 0) return;
|
||||
|
||||
var grid = _mapManager.GetGrid(gridId);
|
||||
var debugResult = new Dictionary<int, List<Vector2>>();
|
||||
|
||||
foreach (var region in regions)
|
||||
{
|
||||
debugResult.Add(_runningCacheIdx, new List<Vector2>());
|
||||
|
||||
foreach (var node in region.Nodes)
|
||||
{
|
||||
var nodeVector = grid.GridTileToLocal(node.TileRef.GridIndices).ToMapPos(EntityManager);
|
||||
|
||||
debugResult[_runningCacheIdx].Add(nodeVector);
|
||||
}
|
||||
|
||||
_runningCacheIdx++;
|
||||
}
|
||||
|
||||
foreach (var session in _subscribedSessions)
|
||||
{
|
||||
RaiseNetworkEvent(new SharedAiDebug.ReachableCacheDebugMessage(gridId, debugResult, cached), session.ConnectedClient);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
67
Content.Server/NPC/Pathfinding/Accessible/BFSPathfinder.cs
Normal file
67
Content.Server/NPC/Pathfinding/Accessible/BFSPathfinder.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using Content.Server.NPC.Pathfinding.Pathfinders;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.NPC.Pathfinding.Accessible
|
||||
{
|
||||
/// <summary>
|
||||
/// The simplest pathfinder
|
||||
/// </summary>
|
||||
public sealed class BFSPathfinder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all of the tiles in range that can we access
|
||||
/// </summary>
|
||||
/// If you want Dijkstra then add distances.
|
||||
/// Doesn't use the JobQueue as it will generally be encapsulated by other jobs
|
||||
/// <param name="pathfindingArgs"></param>
|
||||
/// <param name="range"></param>
|
||||
/// <param name="fromStart">Whether we traverse from the starting tile or the end tile</param>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<PathfindingNode> GetNodesInRange(PathfindingArgs pathfindingArgs, bool fromStart = true)
|
||||
{
|
||||
var pathfindingSystem = EntitySystem.Get<PathfindingSystem>();
|
||||
// Don't need a priority queue given not looking for shortest path
|
||||
var openTiles = new Queue<PathfindingNode>();
|
||||
var closedTiles = new HashSet<TileRef>();
|
||||
PathfindingNode startNode;
|
||||
|
||||
if (fromStart)
|
||||
{
|
||||
startNode = pathfindingSystem.GetNode(pathfindingArgs.Start);
|
||||
}
|
||||
else
|
||||
{
|
||||
startNode = pathfindingSystem.GetNode(pathfindingArgs.End);
|
||||
}
|
||||
|
||||
PathfindingNode currentNode;
|
||||
openTiles.Enqueue(startNode);
|
||||
|
||||
while (openTiles.Count > 0)
|
||||
{
|
||||
currentNode = openTiles.Dequeue();
|
||||
|
||||
foreach (var neighbor in currentNode.GetNeighbors())
|
||||
{
|
||||
// No distances stored so can just check closed tiles here
|
||||
if (closedTiles.Contains(neighbor.TileRef)) continue;
|
||||
closedTiles.Add(currentNode.TileRef);
|
||||
|
||||
// So currently tileCost gets the octile distance between the 2 so we'll also use that for our range check
|
||||
var tileCost = PathfindingHelpers.GetTileCost(pathfindingArgs, startNode, neighbor);
|
||||
var direction = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
||||
|
||||
if (tileCost == null ||
|
||||
tileCost > pathfindingArgs.Proximity ||
|
||||
!PathfindingHelpers.DirectionTraversable(pathfindingArgs.CollisionMask, pathfindingArgs.Access, currentNode, direction))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
openTiles.Enqueue(neighbor);
|
||||
yield return neighbor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
138
Content.Server/NPC/Pathfinding/Accessible/PathfindingRegion.cs
Normal file
138
Content.Server/NPC/Pathfinding/Accessible/PathfindingRegion.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
namespace Content.Server.NPC.Pathfinding.Accessible
|
||||
{
|
||||
/// <summary>
|
||||
/// A group of homogenous PathfindingNodes inside a single chunk
|
||||
/// </summary>
|
||||
/// Makes the graph smaller and quicker to traverse
|
||||
public sealed class PathfindingRegion : IEquatable<PathfindingRegion>
|
||||
{
|
||||
/// <summary>
|
||||
/// Bottom-left reference node of the region
|
||||
/// </summary>
|
||||
public PathfindingNode OriginNode { get; }
|
||||
|
||||
// The shape may be anything within the bounds of a chunk, this is just a quick way to do a bounds-check
|
||||
|
||||
/// <summary>
|
||||
/// Maximum width of the nodes
|
||||
/// </summary>
|
||||
public int Height { get; private set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum width of the nodes
|
||||
/// </summary>
|
||||
public int Width { get; private set; } = 1;
|
||||
|
||||
public PathfindingChunk ParentChunk => OriginNode.ParentChunk;
|
||||
public HashSet<PathfindingRegion> Neighbors { get; } = new();
|
||||
|
||||
public bool IsDoor { get; }
|
||||
public HashSet<PathfindingNode> Nodes => _nodes;
|
||||
private readonly HashSet<PathfindingNode> _nodes;
|
||||
|
||||
public bool Deleted { get; private set; }
|
||||
|
||||
public PathfindingRegion(PathfindingNode originNode, HashSet<PathfindingNode> nodes, bool isDoor = false)
|
||||
{
|
||||
OriginNode = originNode;
|
||||
_nodes = nodes;
|
||||
IsDoor = isDoor;
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
// Tell our neighbors we no longer exist ;-/
|
||||
var neighbors = new List<PathfindingRegion>(Neighbors);
|
||||
|
||||
for (var i = 0; i < neighbors.Count; i++)
|
||||
{
|
||||
var neighbor = neighbors[i];
|
||||
neighbor.Neighbors.Remove(this);
|
||||
}
|
||||
|
||||
_nodes.Clear();
|
||||
Neighbors.Clear();
|
||||
|
||||
Deleted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Roughly how far away another region is by nearest node
|
||||
/// </summary>
|
||||
/// <param name="otherRegion"></param>
|
||||
/// <returns></returns>
|
||||
public float Distance(PathfindingRegion otherRegion)
|
||||
{
|
||||
// JANK
|
||||
var xDistance = otherRegion.OriginNode.TileRef.X - OriginNode.TileRef.X;
|
||||
var yDistance = otherRegion.OriginNode.TileRef.Y - OriginNode.TileRef.Y;
|
||||
|
||||
if (xDistance > 0)
|
||||
{
|
||||
xDistance -= Width;
|
||||
}
|
||||
else if (xDistance < 0)
|
||||
{
|
||||
xDistance = Math.Abs(xDistance + otherRegion.Width);
|
||||
}
|
||||
|
||||
if (yDistance > 0)
|
||||
{
|
||||
yDistance -= Height;
|
||||
}
|
||||
else if (yDistance < 0)
|
||||
{
|
||||
yDistance = Math.Abs(yDistance + otherRegion.Height);
|
||||
}
|
||||
|
||||
return PathfindingHelpers.OctileDistance(xDistance, yDistance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can the given args can traverse this region?
|
||||
/// </summary>
|
||||
/// <param name="reachableArgs"></param>
|
||||
/// <returns></returns>
|
||||
public bool RegionTraversable(ReachableArgs reachableArgs)
|
||||
{
|
||||
// The assumption is that all nodes in a region have the same pathfinding traits
|
||||
// As such we can just use the origin node for checking.
|
||||
return PathfindingHelpers.Traversable(reachableArgs.CollisionMask, reachableArgs.Access,
|
||||
OriginNode);
|
||||
}
|
||||
|
||||
public void Add(PathfindingNode node)
|
||||
{
|
||||
var xWidth = Math.Abs(node.TileRef.X - OriginNode.TileRef.X);
|
||||
var yHeight = Math.Abs(node.TileRef.Y - OriginNode.TileRef.Y);
|
||||
|
||||
if (xWidth > Width)
|
||||
{
|
||||
Width = xWidth;
|
||||
}
|
||||
|
||||
if (yHeight > Height)
|
||||
{
|
||||
Height = yHeight;
|
||||
}
|
||||
|
||||
_nodes.Add(node);
|
||||
}
|
||||
|
||||
// HashSet wasn't working correctly so uhh we got this.
|
||||
public bool Equals(PathfindingRegion? other)
|
||||
{
|
||||
if (other == null) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
if (_nodes.Count != other.Nodes.Count) return false;
|
||||
if (Deleted != other.Deleted) return false;
|
||||
if (OriginNode != other.OriginNode) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return OriginNode.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Content.Server/NPC/Pathfinding/Accessible/ReachableArgs.cs
Normal file
39
Content.Server/NPC/Pathfinding/Accessible/ReachableArgs.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Content.Shared.Access.Systems;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Content.Server.NPC.Pathfinding.Accessible
|
||||
{
|
||||
public sealed class ReachableArgs
|
||||
{
|
||||
public float VisionRadius { get; set; }
|
||||
public ICollection<string> Access { get; }
|
||||
public int CollisionMask { get; }
|
||||
|
||||
public ReachableArgs(float visionRadius, ICollection<string> access, int collisionMask)
|
||||
{
|
||||
VisionRadius = visionRadius;
|
||||
Access = access;
|
||||
CollisionMask = collisionMask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get appropriate args for a particular entity
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <returns></returns>
|
||||
public static ReachableArgs GetArgs(EntityUid entity, float radius)
|
||||
{
|
||||
var collisionMask = 0;
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
if (entMan.TryGetComponent(entity, out IPhysBody? physics))
|
||||
{
|
||||
collisionMask = physics.CollisionMask;
|
||||
}
|
||||
|
||||
var accessSystem = EntitySystem.Get<AccessReaderSystem>();
|
||||
var access = accessSystem.FindAccessTags(entity);
|
||||
|
||||
return new ReachableArgs(radius, access, collisionMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Content.Server.NPC.Pathfinding;
|
||||
|
||||
[RegisterComponent]
|
||||
[Access(typeof(PathfindingSystem))]
|
||||
public sealed class GridPathfindingComponent : Component
|
||||
{
|
||||
public readonly Dictionary<Vector2i, PathfindingChunk> Graph = new();
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.CPUJob.JobQueues;
|
||||
using Content.Shared.AI;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.NPC.Pathfinding.Pathfinders
|
||||
{
|
||||
public sealed class AStarPathfindingJob : Job<Queue<TileRef>>
|
||||
{
|
||||
#if DEBUG
|
||||
public static event Action<SharedAiDebug.AStarRouteDebug>? DebugRoute;
|
||||
#endif
|
||||
|
||||
private readonly PathfindingNode _startNode;
|
||||
private PathfindingNode _endNode;
|
||||
private readonly PathfindingArgs _pathfindingArgs;
|
||||
private readonly IEntityManager _entityManager;
|
||||
|
||||
public AStarPathfindingJob(
|
||||
double maxTime,
|
||||
PathfindingNode startNode,
|
||||
PathfindingNode endNode,
|
||||
PathfindingArgs pathfindingArgs,
|
||||
CancellationToken cancellationToken,
|
||||
IEntityManager entityManager) : base(maxTime, cancellationToken)
|
||||
{
|
||||
_startNode = startNode;
|
||||
_endNode = endNode;
|
||||
_pathfindingArgs = pathfindingArgs;
|
||||
_entityManager = entityManager;
|
||||
}
|
||||
|
||||
protected override async Task<Queue<TileRef>?> Process()
|
||||
{
|
||||
if (_startNode.TileRef.Equals(TileRef.Zero) ||
|
||||
_endNode.TileRef.Equals(TileRef.Zero) ||
|
||||
Status == JobStatus.Finished)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we couldn't get a nearby node that's good enough
|
||||
if (!PathfindingHelpers.TryEndNode(ref _endNode, _pathfindingArgs))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_entityManager.Deleted(_pathfindingArgs.Start.GridUid))
|
||||
return null;
|
||||
|
||||
var frontier = new PriorityQueue<ValueTuple<float, PathfindingNode>>(new PathfindingComparer());
|
||||
var costSoFar = new Dictionary<PathfindingNode, float>();
|
||||
var cameFrom = new Dictionary<PathfindingNode, PathfindingNode>();
|
||||
|
||||
PathfindingNode? currentNode = null;
|
||||
frontier.Add((0.0f, _startNode));
|
||||
costSoFar[_startNode] = 0.0f;
|
||||
var routeFound = false;
|
||||
var count = 0;
|
||||
|
||||
while (frontier.Count > 0)
|
||||
{
|
||||
// Handle whether we need to pause if we've taken too long
|
||||
count++;
|
||||
if (count % 20 == 0 && count > 0)
|
||||
{
|
||||
await SuspendIfOutOfTime();
|
||||
|
||||
if (_startNode == null || _endNode == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Actual pathfinding here
|
||||
(_, currentNode) = frontier.Take();
|
||||
if (currentNode.Equals(_endNode))
|
||||
{
|
||||
routeFound = true;
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var nextNode in currentNode.GetNeighbors())
|
||||
{
|
||||
// If tile is untraversable it'll be null
|
||||
var tileCost = PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, nextNode);
|
||||
if (tileCost == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// So if we're going NE then that means either N or E needs to be free to actually get there
|
||||
var direction = PathfindingHelpers.RelativeDirection(nextNode, currentNode);
|
||||
if (!PathfindingHelpers.DirectionTraversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, currentNode, direction))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// f = g + h
|
||||
// gScore is distance to the start node
|
||||
// hScore is distance to the end node
|
||||
var gScore = costSoFar[currentNode] + tileCost.Value;
|
||||
if (costSoFar.TryGetValue(nextNode, out var nextValue) && gScore >= nextValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cameFrom[nextNode] = currentNode;
|
||||
costSoFar[nextNode] = gScore;
|
||||
// pFactor is tie-breaker where the fscore is otherwise equal.
|
||||
// See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
|
||||
// There's other ways to do it but future consideration
|
||||
// The closer the fScore is to the actual distance then the better the pathfinder will be
|
||||
// (i.e. somewhere between 1 and infinite)
|
||||
// Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
|
||||
var fScore = gScore + PathfindingHelpers.OctileDistance(_endNode, nextNode) * (1.0f + 1.0f / 1000.0f);
|
||||
frontier.Add((fScore, nextNode));
|
||||
}
|
||||
}
|
||||
|
||||
if (!routeFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
DebugTools.AssertNotNull(currentNode);
|
||||
|
||||
var route = PathfindingHelpers.ReconstructPath(cameFrom, currentNode!);
|
||||
|
||||
if (route.Count == 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var simplifiedRoute = PathfindingSystem.Simplify(route, 0f);
|
||||
var actualRoute = new Queue<TileRef>(simplifiedRoute);
|
||||
|
||||
#if DEBUG
|
||||
// Need to get data into an easier format to send to the relevant clients
|
||||
if (DebugRoute != null && route.Count > 0)
|
||||
{
|
||||
var debugCameFrom = new Dictionary<TileRef, TileRef>(cameFrom.Count);
|
||||
var debugGScores = new Dictionary<TileRef, float>(costSoFar.Count);
|
||||
foreach (var (node, parent) in cameFrom)
|
||||
{
|
||||
debugCameFrom.Add(node.TileRef, parent.TileRef);
|
||||
}
|
||||
|
||||
foreach (var (node, score) in costSoFar)
|
||||
{
|
||||
debugGScores.Add(node.TileRef, score);
|
||||
}
|
||||
|
||||
var debugRoute = new SharedAiDebug.AStarRouteDebug(
|
||||
_pathfindingArgs.Uid,
|
||||
actualRoute,
|
||||
debugCameFrom,
|
||||
debugGScores,
|
||||
DebugTime);
|
||||
|
||||
DebugRoute.Invoke(debugRoute);
|
||||
}
|
||||
#endif
|
||||
|
||||
return actualRoute;
|
||||
}
|
||||
}
|
||||
}
|
||||
512
Content.Server/NPC/Pathfinding/Pathfinders/JpsPathfindingJob.cs
Normal file
512
Content.Server/NPC/Pathfinding/Pathfinders/JpsPathfindingJob.cs
Normal file
@@ -0,0 +1,512 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.CPUJob.JobQueues;
|
||||
using Content.Shared.AI;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.NPC.Pathfinding.Pathfinders
|
||||
{
|
||||
public sealed class JpsPathfindingJob : Job<Queue<TileRef>>
|
||||
{
|
||||
// Some of this is probably fugly due to other structural changes in pathfinding so it could do with optimisation
|
||||
// Realistically it's probably not getting used given it doesn't support tile costs which can be very useful
|
||||
#if DEBUG
|
||||
public static event Action<SharedAiDebug.JpsRouteDebug>? DebugRoute;
|
||||
#endif
|
||||
|
||||
private readonly PathfindingNode? _startNode;
|
||||
private PathfindingNode? _endNode;
|
||||
private readonly PathfindingArgs _pathfindingArgs;
|
||||
|
||||
public JpsPathfindingJob(double maxTime,
|
||||
PathfindingNode startNode,
|
||||
PathfindingNode endNode,
|
||||
PathfindingArgs pathfindingArgs,
|
||||
CancellationToken cancellationToken) : base(maxTime, cancellationToken)
|
||||
{
|
||||
_startNode = startNode;
|
||||
_endNode = endNode;
|
||||
_pathfindingArgs = pathfindingArgs;
|
||||
}
|
||||
|
||||
protected override async Task<Queue<TileRef>?> Process()
|
||||
{
|
||||
// VERY similar to A*; main difference is with the neighbor tiles you look for jump nodes instead
|
||||
if (_startNode == null ||
|
||||
_endNode == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we couldn't get a nearby node that's good enough
|
||||
if (!PathfindingHelpers.TryEndNode(ref _endNode, _pathfindingArgs))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var openTiles = new PriorityQueue<ValueTuple<float, PathfindingNode>>(new PathfindingComparer());
|
||||
var gScores = new Dictionary<PathfindingNode, float>();
|
||||
var cameFrom = new Dictionary<PathfindingNode, PathfindingNode>();
|
||||
var closedTiles = new HashSet<PathfindingNode>();
|
||||
|
||||
#if DEBUG
|
||||
var jumpNodes = new HashSet<PathfindingNode>();
|
||||
#endif
|
||||
|
||||
PathfindingNode? currentNode = null;
|
||||
openTiles.Add((0, _startNode));
|
||||
gScores[_startNode] = 0.0f;
|
||||
var routeFound = false;
|
||||
var count = 0;
|
||||
|
||||
while (openTiles.Count > 0)
|
||||
{
|
||||
count++;
|
||||
|
||||
// JPS probably getting a lot fewer nodes than A* is
|
||||
if (count % 5 == 0 && count > 0)
|
||||
{
|
||||
await SuspendIfOutOfTime();
|
||||
}
|
||||
|
||||
(_, currentNode) = openTiles.Take();
|
||||
if (currentNode.Equals(_endNode))
|
||||
{
|
||||
routeFound = true;
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var node in currentNode.GetNeighbors())
|
||||
{
|
||||
var direction = PathfindingHelpers.RelativeDirection(node, currentNode);
|
||||
var jumpNode = GetJumpPoint(currentNode, direction, _endNode);
|
||||
|
||||
if (jumpNode != null && !closedTiles.Contains(jumpNode))
|
||||
{
|
||||
closedTiles.Add(jumpNode);
|
||||
#if DEBUG
|
||||
jumpNodes.Add(jumpNode);
|
||||
#endif
|
||||
// GetJumpPoint should already check if we can traverse to the node
|
||||
var tileCost = PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, jumpNode);
|
||||
|
||||
if (tileCost == null)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
var gScore = gScores[currentNode] + tileCost.Value;
|
||||
|
||||
if (gScores.TryGetValue(jumpNode, out var nextValue) && gScore >= nextValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cameFrom[jumpNode] = currentNode;
|
||||
gScores[jumpNode] = gScore;
|
||||
// pFactor is tie-breaker where the fscore is otherwise equal.
|
||||
// See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
|
||||
// There's other ways to do it but future consideration
|
||||
var fScore = gScores[jumpNode] + PathfindingHelpers.OctileDistance(_endNode, jumpNode) * (1.0f + 1.0f / 1000.0f);
|
||||
openTiles.Add((fScore, jumpNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!routeFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
DebugTools.AssertNotNull(currentNode);
|
||||
|
||||
var route = PathfindingHelpers.ReconstructJumpPath(cameFrom, currentNode!);
|
||||
|
||||
if (route.Count == 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
// Need to get data into an easier format to send to the relevant clients
|
||||
if (DebugRoute != null && route.Count > 0)
|
||||
{
|
||||
var debugJumpNodes = new HashSet<TileRef>(jumpNodes.Count);
|
||||
|
||||
foreach (var node in jumpNodes)
|
||||
{
|
||||
debugJumpNodes.Add(node.TileRef);
|
||||
}
|
||||
|
||||
var debugRoute = new SharedAiDebug.JpsRouteDebug(
|
||||
_pathfindingArgs.Uid,
|
||||
route,
|
||||
debugJumpNodes,
|
||||
DebugTime);
|
||||
|
||||
DebugRoute.Invoke(debugRoute);
|
||||
}
|
||||
#endif
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
private PathfindingNode? GetJumpPoint(PathfindingNode currentNode, Direction direction, PathfindingNode endNode)
|
||||
{
|
||||
var count = 0;
|
||||
|
||||
while (count < 1000)
|
||||
{
|
||||
count++;
|
||||
PathfindingNode? nextNode = null;
|
||||
foreach (var node in currentNode.GetNeighbors())
|
||||
{
|
||||
if (PathfindingHelpers.RelativeDirection(node, currentNode) == direction)
|
||||
{
|
||||
nextNode = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We'll do opposite DirectionTraversable just because of how the method's setup
|
||||
// Nodes should be 2-way anyway.
|
||||
if (nextNode == null ||
|
||||
PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, nextNode) == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (nextNode == endNode)
|
||||
{
|
||||
return endNode;
|
||||
}
|
||||
|
||||
// Horizontal and vertical are treated the same i.e.
|
||||
// They only check in their specific direction
|
||||
// (So Going North means you check NorthWest and NorthEast to see if we're a jump point)
|
||||
|
||||
// Diagonals also check the cardinal directions at the same time at the same time
|
||||
|
||||
// See https://harablog.wordpress.com/2011/09/07/jump-point-search/ for original description
|
||||
switch (direction)
|
||||
{
|
||||
case Direction.East:
|
||||
if (IsCardinalJumpPoint(direction, nextNode))
|
||||
{
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
break;
|
||||
case Direction.NorthEast:
|
||||
if (IsDiagonalJumpPoint(direction, nextNode))
|
||||
{
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
if (GetJumpPoint(nextNode, Direction.North, endNode) != null || GetJumpPoint(nextNode, Direction.East, endNode) != null)
|
||||
{
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
break;
|
||||
case Direction.North:
|
||||
if (IsCardinalJumpPoint(direction, nextNode))
|
||||
{
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
break;
|
||||
case Direction.NorthWest:
|
||||
if (IsDiagonalJumpPoint(direction, nextNode))
|
||||
{
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
if (GetJumpPoint(nextNode, Direction.North, endNode) != null || GetJumpPoint(nextNode, Direction.West, endNode) != null)
|
||||
{
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
break;
|
||||
case Direction.West:
|
||||
if (IsCardinalJumpPoint(direction, nextNode))
|
||||
{
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
break;
|
||||
case Direction.SouthWest:
|
||||
if (IsDiagonalJumpPoint(direction, nextNode))
|
||||
{
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
if (GetJumpPoint(nextNode, Direction.South, endNode) != null || GetJumpPoint(nextNode, Direction.West, endNode) != null)
|
||||
{
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
break;
|
||||
case Direction.South:
|
||||
if (IsCardinalJumpPoint(direction, nextNode))
|
||||
{
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
break;
|
||||
case Direction.SouthEast:
|
||||
if (IsDiagonalJumpPoint(direction, nextNode))
|
||||
{
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
if (GetJumpPoint(nextNode, Direction.South, endNode) != null || GetJumpPoint(nextNode, Direction.East, endNode) != null)
|
||||
{
|
||||
return nextNode;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(direction), direction, null);
|
||||
}
|
||||
|
||||
currentNode = nextNode;
|
||||
}
|
||||
|
||||
Logger.WarningS("pathfinding", "Recursion found in JPS pathfinder");
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsDiagonalJumpPoint(Direction direction, PathfindingNode currentNode)
|
||||
{
|
||||
// If we're going diagonally need to check all cardinals.
|
||||
// I tried just casting direction ints and offsets to make it smaller but brain no worky.
|
||||
// From NorthEast we check (Closed / Open) S - SE, W - NW
|
||||
|
||||
PathfindingNode? openNeighborOne = null;
|
||||
PathfindingNode? closedNeighborOne = null;
|
||||
PathfindingNode? openNeighborTwo = null;
|
||||
PathfindingNode? closedNeighborTwo = null;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case Direction.NorthEast:
|
||||
foreach (var neighbor in currentNode.GetNeighbors())
|
||||
{
|
||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
||||
switch (neighborDirection)
|
||||
{
|
||||
case Direction.SouthEast:
|
||||
openNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.South:
|
||||
closedNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.NorthWest:
|
||||
openNeighborTwo = neighbor;
|
||||
break;
|
||||
case Direction.West:
|
||||
closedNeighborTwo = neighbor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Direction.SouthEast:
|
||||
foreach (var neighbor in currentNode.GetNeighbors())
|
||||
{
|
||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
||||
switch (neighborDirection)
|
||||
{
|
||||
case Direction.NorthEast:
|
||||
openNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.North:
|
||||
closedNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.SouthWest:
|
||||
openNeighborTwo = neighbor;
|
||||
break;
|
||||
case Direction.West:
|
||||
closedNeighborTwo = neighbor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Direction.SouthWest:
|
||||
foreach (var neighbor in currentNode.GetNeighbors())
|
||||
{
|
||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
||||
switch (neighborDirection)
|
||||
{
|
||||
case Direction.NorthWest:
|
||||
openNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.North:
|
||||
closedNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.SouthEast:
|
||||
openNeighborTwo = neighbor;
|
||||
break;
|
||||
case Direction.East:
|
||||
closedNeighborTwo = neighbor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Direction.NorthWest:
|
||||
foreach (var neighbor in currentNode.GetNeighbors())
|
||||
{
|
||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
||||
switch (neighborDirection)
|
||||
{
|
||||
case Direction.SouthWest:
|
||||
openNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.South:
|
||||
closedNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.NorthEast:
|
||||
openNeighborTwo = neighbor;
|
||||
break;
|
||||
case Direction.East:
|
||||
closedNeighborTwo = neighbor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
if ((closedNeighborOne == null || PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, closedNeighborOne) == null)
|
||||
&& openNeighborOne != null && PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, openNeighborOne) != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((closedNeighborTwo == null || PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, closedNeighborTwo) == null)
|
||||
&& openNeighborTwo != null && PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, openNeighborTwo) != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check to see if the node is a jump point (only works for cardinal directions)
|
||||
/// </summary>
|
||||
private bool IsCardinalJumpPoint(Direction direction, PathfindingNode currentNode)
|
||||
{
|
||||
PathfindingNode? openNeighborOne = null;
|
||||
PathfindingNode? closedNeighborOne = null;
|
||||
PathfindingNode? openNeighborTwo = null;
|
||||
PathfindingNode? closedNeighborTwo = null;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case Direction.North:
|
||||
foreach (var neighbor in currentNode.GetNeighbors())
|
||||
{
|
||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
||||
switch (neighborDirection)
|
||||
{
|
||||
case Direction.NorthEast:
|
||||
openNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.East:
|
||||
closedNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.NorthWest:
|
||||
openNeighborTwo = neighbor;
|
||||
break;
|
||||
case Direction.West:
|
||||
closedNeighborTwo = neighbor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Direction.East:
|
||||
foreach (var neighbor in currentNode.GetNeighbors())
|
||||
{
|
||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
||||
switch (neighborDirection)
|
||||
{
|
||||
case Direction.NorthEast:
|
||||
openNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.North:
|
||||
closedNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.SouthEast:
|
||||
openNeighborTwo = neighbor;
|
||||
break;
|
||||
case Direction.South:
|
||||
closedNeighborTwo = neighbor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Direction.South:
|
||||
foreach (var neighbor in currentNode.GetNeighbors())
|
||||
{
|
||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
||||
switch (neighborDirection)
|
||||
{
|
||||
case Direction.SouthEast:
|
||||
openNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.East:
|
||||
closedNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.SouthWest:
|
||||
openNeighborTwo = neighbor;
|
||||
break;
|
||||
case Direction.West:
|
||||
closedNeighborTwo = neighbor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Direction.West:
|
||||
foreach (var neighbor in currentNode.GetNeighbors())
|
||||
{
|
||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
||||
switch (neighborDirection)
|
||||
{
|
||||
case Direction.NorthWest:
|
||||
openNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.North:
|
||||
closedNeighborOne = neighbor;
|
||||
break;
|
||||
case Direction.SouthWest:
|
||||
openNeighborTwo = neighbor;
|
||||
break;
|
||||
case Direction.South:
|
||||
closedNeighborTwo = neighbor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
if ((closedNeighborOne == null || !PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, closedNeighborOne)) &&
|
||||
openNeighborOne != null && PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, openNeighborOne))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((closedNeighborTwo == null || !PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, closedNeighborTwo)) &&
|
||||
openNeighborTwo != null && PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, openNeighborTwo))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.NPC.Pathfinding.Pathfinders
|
||||
{
|
||||
public struct PathfindingArgs
|
||||
{
|
||||
public EntityUid Uid { get; }
|
||||
public ICollection<string> Access { get; }
|
||||
public int CollisionMask { get; }
|
||||
public TileRef Start { get; }
|
||||
public TileRef End { get; }
|
||||
// How close we need to get to the endpoint to be 'done'
|
||||
public float Proximity { get; }
|
||||
// Whether we use cardinal only or not
|
||||
public bool AllowDiagonals { get; }
|
||||
// Can we go through walls
|
||||
public bool NoClip { get; }
|
||||
// Can we traverse space tiles
|
||||
public bool AllowSpace { get; }
|
||||
|
||||
public PathfindingArgs(
|
||||
EntityUid entityUid,
|
||||
ICollection<string> access,
|
||||
int collisionMask,
|
||||
TileRef start,
|
||||
TileRef end,
|
||||
float proximity = 0.0f,
|
||||
bool allowDiagonals = true,
|
||||
bool noClip = false,
|
||||
bool allowSpace = false)
|
||||
{
|
||||
Uid = entityUid;
|
||||
Access = access;
|
||||
CollisionMask = collisionMask;
|
||||
Start = start;
|
||||
End = end;
|
||||
Proximity = proximity;
|
||||
AllowDiagonals = allowDiagonals;
|
||||
NoClip = noClip;
|
||||
AllowSpace = allowSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Content.Server.NPC.Pathfinding.Pathfinders
|
||||
{
|
||||
public sealed class PathfindingComparer : IComparer<ValueTuple<float, PathfindingNode>>
|
||||
{
|
||||
public int Compare((float, PathfindingNode) x, (float, PathfindingNode) y)
|
||||
{
|
||||
return y.Item1.CompareTo(x.Item1);
|
||||
}
|
||||
}
|
||||
}
|
||||
185
Content.Server/NPC/Pathfinding/PathfindingChunk.cs
Normal file
185
Content.Server/NPC/Pathfinding/PathfindingChunk.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System.Linq;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.NPC.Pathfinding
|
||||
{
|
||||
public sealed class PathfindingChunkUpdateMessage : EntityEventArgs
|
||||
{
|
||||
public PathfindingChunk Chunk { get; }
|
||||
|
||||
public PathfindingChunkUpdateMessage(PathfindingChunk chunk)
|
||||
{
|
||||
Chunk = chunk;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PathfindingChunk
|
||||
{
|
||||
public TimeSpan LastUpdate { get; private set; }
|
||||
public EntityUid GridId { get; }
|
||||
|
||||
public Vector2i Indices => _indices;
|
||||
private readonly Vector2i _indices;
|
||||
|
||||
// Nodes per chunk row
|
||||
public static int ChunkSize => 8;
|
||||
public PathfindingNode[,] Nodes => _nodes;
|
||||
private readonly PathfindingNode[,] _nodes = new PathfindingNode[ChunkSize,ChunkSize];
|
||||
|
||||
public PathfindingChunk(EntityUid gridId, Vector2i indices)
|
||||
{
|
||||
GridId = gridId;
|
||||
_indices = indices;
|
||||
}
|
||||
|
||||
public void Initialize(IMapGrid mapGrid)
|
||||
{
|
||||
for (var x = 0; x < ChunkSize; x++)
|
||||
{
|
||||
for (var y = 0; y < ChunkSize; y++)
|
||||
{
|
||||
var tileRef = mapGrid.GetTileRef(new Vector2i(x + _indices.X, y + _indices.Y));
|
||||
CreateNode(tileRef);
|
||||
}
|
||||
}
|
||||
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only called when blockers change (i.e. un-anchored physics objects don't trigger)
|
||||
/// </summary>
|
||||
public void Dirty()
|
||||
{
|
||||
LastUpdate = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
IoCManager.Resolve<IEntityManager>().EventBus
|
||||
.RaiseEvent(EventSource.Local, new PathfindingChunkUpdateMessage(this));
|
||||
}
|
||||
|
||||
public IEnumerable<PathfindingChunk> GetNeighbors(IEntityManager? entManager = null)
|
||||
{
|
||||
IoCManager.Resolve(ref entManager);
|
||||
var chunkGrid = entManager.GetComponent<GridPathfindingComponent>(GridId).Graph;
|
||||
|
||||
for (var x = -1; x <= 1; x++)
|
||||
{
|
||||
for (var y = -1; y <= 1; y++)
|
||||
{
|
||||
if (x == 0 && y == 0) continue;
|
||||
var (neighborX, neighborY) = (_indices.X + ChunkSize * x, _indices.Y + ChunkSize * y);
|
||||
if (chunkGrid.TryGetValue(new Vector2i(neighborX, neighborY), out var neighbor))
|
||||
{
|
||||
yield return neighbor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool InBounds(Vector2i vector)
|
||||
{
|
||||
if (vector.X < _indices.X || vector.Y < _indices.Y) return false;
|
||||
if (vector.X >= _indices.X + ChunkSize || vector.Y >= _indices.Y + ChunkSize) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the tile is on the outer edge
|
||||
/// </summary>
|
||||
/// <param name="node"></param>
|
||||
/// <returns></returns>
|
||||
public bool OnEdge(PathfindingNode node)
|
||||
{
|
||||
if (node.TileRef.X == _indices.X) return true;
|
||||
if (node.TileRef.Y == _indices.Y) return true;
|
||||
if (node.TileRef.X == _indices.X + ChunkSize - 1) return true;
|
||||
if (node.TileRef.Y == _indices.Y + ChunkSize - 1) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets our neighbors that are relevant for the node to retrieve its own neighbors
|
||||
/// </summary>
|
||||
/// <param name="node"></param>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<PathfindingChunk> RelevantChunks(PathfindingNode node)
|
||||
{
|
||||
var relevantDirections = GetEdges(node).ToList();
|
||||
|
||||
foreach (var chunk in GetNeighbors())
|
||||
{
|
||||
var chunkDirection = PathfindingHelpers.RelativeDirection(chunk, this);
|
||||
if (relevantDirections.Contains(chunkDirection))
|
||||
{
|
||||
yield return chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Direction> GetEdges(PathfindingNode node)
|
||||
{
|
||||
// West Edge
|
||||
if (node.TileRef.X == _indices.X)
|
||||
{
|
||||
yield return Direction.West;
|
||||
if (node.TileRef.Y == _indices.Y)
|
||||
{
|
||||
yield return Direction.SouthWest;
|
||||
yield return Direction.South;
|
||||
} else if (node.TileRef.Y == _indices.Y + ChunkSize - 1)
|
||||
{
|
||||
yield return Direction.NorthWest;
|
||||
yield return Direction.North;
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
// East edge
|
||||
if (node.TileRef.X == _indices.X + ChunkSize - 1)
|
||||
{
|
||||
yield return Direction.East;
|
||||
if (node.TileRef.Y == _indices.Y)
|
||||
{
|
||||
yield return Direction.SouthEast;
|
||||
yield return Direction.South;
|
||||
} else if (node.TileRef.Y == _indices.Y + ChunkSize - 1)
|
||||
{
|
||||
yield return Direction.NorthEast;
|
||||
yield return Direction.North;
|
||||
}
|
||||
|
||||
yield break;
|
||||
|
||||
}
|
||||
// South edge
|
||||
if (node.TileRef.Y == _indices.Y)
|
||||
{
|
||||
yield return Direction.South;
|
||||
// Given we already checked south-west and south-east above shouldn't need any more
|
||||
}
|
||||
// North edge
|
||||
if (node.TileRef.Y == _indices.Y + ChunkSize - 1)
|
||||
{
|
||||
yield return Direction.North;
|
||||
}
|
||||
}
|
||||
|
||||
public PathfindingNode GetNode(TileRef tile)
|
||||
{
|
||||
var chunkX = tile.X - _indices.X;
|
||||
var chunkY = tile.Y - _indices.Y;
|
||||
|
||||
return _nodes[chunkX, chunkY];
|
||||
}
|
||||
|
||||
private void CreateNode(TileRef tile, PathfindingChunk? parent = null)
|
||||
{
|
||||
parent ??= this;
|
||||
|
||||
var node = new PathfindingNode(parent, tile);
|
||||
var offsetX = tile.X - Indices.X;
|
||||
var offsetY = tile.Y - Indices.Y;
|
||||
_nodes[offsetX, offsetY] = node;
|
||||
}
|
||||
}
|
||||
}
|
||||
350
Content.Server/NPC/Pathfinding/PathfindingHelpers.cs
Normal file
350
Content.Server/NPC/Pathfinding/PathfindingHelpers.cs
Normal file
@@ -0,0 +1,350 @@
|
||||
using Content.Server.NPC.Pathfinding.Accessible;
|
||||
using Content.Server.NPC.Pathfinding.Pathfinders;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.NPC.Pathfinding
|
||||
{
|
||||
public static class PathfindingHelpers
|
||||
{
|
||||
public static bool TryEndNode(ref PathfindingNode endNode, PathfindingArgs pathfindingArgs)
|
||||
{
|
||||
if (!Traversable(pathfindingArgs.CollisionMask, pathfindingArgs.Access, endNode))
|
||||
{
|
||||
if (pathfindingArgs.Proximity > 0.0f)
|
||||
{
|
||||
foreach (var node in BFSPathfinder.GetNodesInRange(pathfindingArgs, false))
|
||||
{
|
||||
endNode = node;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool DirectionTraversable(int collisionMask, ICollection<string> access, PathfindingNode currentNode, Direction direction)
|
||||
{
|
||||
// If it's a diagonal we need to check NSEW to see if we can get to it and stop corner cutting, NE needs N and E etc.
|
||||
// Given there's different collision layers stored for each node in the graph it's probably not worth it to cache this
|
||||
// Also this will help with corner-cutting
|
||||
|
||||
PathfindingNode? northNeighbor = null;
|
||||
PathfindingNode? southNeighbor = null;
|
||||
PathfindingNode? eastNeighbor = null;
|
||||
PathfindingNode? westNeighbor = null;
|
||||
foreach (var neighbor in currentNode.GetNeighbors())
|
||||
{
|
||||
if (neighbor.TileRef.X == currentNode.TileRef.X &&
|
||||
neighbor.TileRef.Y == currentNode.TileRef.Y + 1)
|
||||
{
|
||||
northNeighbor = neighbor;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (neighbor.TileRef.X == currentNode.TileRef.X + 1 &&
|
||||
neighbor.TileRef.Y == currentNode.TileRef.Y)
|
||||
{
|
||||
eastNeighbor = neighbor;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (neighbor.TileRef.X == currentNode.TileRef.X &&
|
||||
neighbor.TileRef.Y == currentNode.TileRef.Y - 1)
|
||||
{
|
||||
southNeighbor = neighbor;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (neighbor.TileRef.X == currentNode.TileRef.X - 1 &&
|
||||
neighbor.TileRef.Y == currentNode.TileRef.Y)
|
||||
{
|
||||
westNeighbor = neighbor;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case Direction.NorthEast:
|
||||
if (northNeighbor == null || eastNeighbor == null) return false;
|
||||
if (!Traversable(collisionMask, access, northNeighbor) ||
|
||||
!Traversable(collisionMask, access, eastNeighbor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case Direction.NorthWest:
|
||||
if (northNeighbor == null || westNeighbor == null) return false;
|
||||
if (!Traversable(collisionMask, access, northNeighbor) ||
|
||||
!Traversable(collisionMask, access, westNeighbor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case Direction.SouthWest:
|
||||
if (southNeighbor == null || westNeighbor == null) return false;
|
||||
if (!Traversable(collisionMask, access, southNeighbor) ||
|
||||
!Traversable(collisionMask, access, westNeighbor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case Direction.SouthEast:
|
||||
if (southNeighbor == null || eastNeighbor == null) return false;
|
||||
if (!Traversable(collisionMask, access, southNeighbor) ||
|
||||
!Traversable(collisionMask, access, eastNeighbor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool Traversable(int collisionMask, ICollection<string> access, PathfindingNode node)
|
||||
{
|
||||
if ((collisionMask & node.BlockedCollisionMask) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var accessSystem = EntitySystem.Get<AccessReaderSystem>();
|
||||
foreach (var reader in node.AccessReaders)
|
||||
{
|
||||
if (!accessSystem.IsAllowed(access, reader))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static List<TileRef> ReconstructPath(Dictionary<PathfindingNode, PathfindingNode> cameFrom, PathfindingNode current)
|
||||
{
|
||||
var running = new Stack<TileRef>();
|
||||
running.Push(current.TileRef);
|
||||
while (cameFrom.ContainsKey(current))
|
||||
{
|
||||
var previousCurrent = current;
|
||||
current = cameFrom[current];
|
||||
cameFrom.Remove(previousCurrent);
|
||||
running.Push(current.TileRef);
|
||||
}
|
||||
|
||||
var result = new List<TileRef>(running);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will reconstruct the path and fill in the tile holes as well
|
||||
/// </summary>
|
||||
/// <param name="cameFrom"></param>
|
||||
/// <param name="current"></param>
|
||||
/// <returns></returns>
|
||||
public static Queue<TileRef> ReconstructJumpPath(Dictionary<PathfindingNode, PathfindingNode> cameFrom, PathfindingNode current)
|
||||
{
|
||||
var running = new Stack<TileRef>();
|
||||
running.Push(current.TileRef);
|
||||
while (cameFrom.ContainsKey(current))
|
||||
{
|
||||
var previousCurrent = current;
|
||||
current = cameFrom[current];
|
||||
var intermediate = previousCurrent;
|
||||
cameFrom.Remove(previousCurrent);
|
||||
var pathfindingSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<PathfindingSystem>();
|
||||
var mapManager = IoCManager.Resolve<IMapManager>();
|
||||
var grid = mapManager.GetGrid(current.TileRef.GridUid);
|
||||
|
||||
// Get all the intermediate nodes
|
||||
while (true)
|
||||
{
|
||||
var xOffset = 0;
|
||||
var yOffset = 0;
|
||||
|
||||
if (intermediate.TileRef.X < current.TileRef.X)
|
||||
{
|
||||
xOffset += 1;
|
||||
}
|
||||
else if (intermediate.TileRef.X > current.TileRef.X)
|
||||
{
|
||||
xOffset -= 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
xOffset = 0;
|
||||
}
|
||||
|
||||
if (intermediate.TileRef.Y < current.TileRef.Y)
|
||||
{
|
||||
yOffset += 1;
|
||||
}
|
||||
else if (intermediate.TileRef.Y > current.TileRef.Y)
|
||||
{
|
||||
yOffset -= 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
yOffset = 0;
|
||||
}
|
||||
|
||||
intermediate = pathfindingSystem.GetNode(grid.GetTileRef(
|
||||
new Vector2i(intermediate.TileRef.X + xOffset, intermediate.TileRef.Y + yOffset)));
|
||||
|
||||
if (intermediate.TileRef != current.TileRef)
|
||||
{
|
||||
// Hacky corner cut fix
|
||||
|
||||
running.Push(intermediate.TileRef);
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
running.Push(current.TileRef);
|
||||
}
|
||||
|
||||
var result = new Queue<TileRef>(running);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static float OctileDistance(int dstX, int dstY)
|
||||
{
|
||||
if (dstX > dstY)
|
||||
{
|
||||
return 1.4f * dstY + (dstX - dstY);
|
||||
}
|
||||
|
||||
return 1.4f * dstX + (dstY - dstX);
|
||||
}
|
||||
|
||||
public static float OctileDistance(PathfindingNode endNode, PathfindingNode currentNode)
|
||||
{
|
||||
// "Fast Euclidean" / octile.
|
||||
// This implementation is written down in a few sources; it just saves doing sqrt.
|
||||
int dstX = Math.Abs(currentNode.TileRef.X - endNode.TileRef.X);
|
||||
int dstY = Math.Abs(currentNode.TileRef.Y - endNode.TileRef.Y);
|
||||
if (dstX > dstY)
|
||||
{
|
||||
return 1.4f * dstY + (dstX - dstY);
|
||||
}
|
||||
|
||||
return 1.4f * dstX + (dstY - dstX);
|
||||
}
|
||||
|
||||
public static float OctileDistance(TileRef endTile, TileRef startTile)
|
||||
{
|
||||
// "Fast Euclidean" / octile.
|
||||
// This implementation is written down in a few sources; it just saves doing sqrt.
|
||||
int dstX = Math.Abs(startTile.X - endTile.X);
|
||||
int dstY = Math.Abs(startTile.Y - endTile.Y);
|
||||
if (dstX > dstY)
|
||||
{
|
||||
return 1.4f * dstY + (dstX - dstY);
|
||||
}
|
||||
|
||||
return 1.4f * dstX + (dstY - dstX);
|
||||
}
|
||||
|
||||
public static float ManhattanDistance(PathfindingNode endNode, PathfindingNode currentNode)
|
||||
{
|
||||
return Math.Abs(currentNode.TileRef.X - endNode.TileRef.X) + Math.Abs(currentNode.TileRef.Y - endNode.TileRef.Y);
|
||||
}
|
||||
|
||||
public static float? GetTileCost(PathfindingArgs pathfindingArgs, PathfindingNode start, PathfindingNode end)
|
||||
{
|
||||
if (!pathfindingArgs.NoClip && !Traversable(pathfindingArgs.CollisionMask, pathfindingArgs.Access, end))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!pathfindingArgs.AllowSpace && end.TileRef.Tile.IsEmpty)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cost = 1.0f;
|
||||
|
||||
switch (pathfindingArgs.AllowDiagonals)
|
||||
{
|
||||
case true:
|
||||
cost *= OctileDistance(end, start);
|
||||
break;
|
||||
// Manhattan distance
|
||||
case false:
|
||||
cost *= ManhattanDistance(end, start);
|
||||
break;
|
||||
}
|
||||
|
||||
return cost;
|
||||
}
|
||||
|
||||
public static Direction RelativeDirection(PathfindingChunk endChunk, PathfindingChunk startChunk)
|
||||
{
|
||||
var xDiff = (endChunk.Indices.X - startChunk.Indices.X) / PathfindingChunk.ChunkSize;
|
||||
var yDiff = (endChunk.Indices.Y - startChunk.Indices.Y) / PathfindingChunk.ChunkSize;
|
||||
|
||||
return RelativeDirection(xDiff, yDiff);
|
||||
}
|
||||
|
||||
public static Direction RelativeDirection(PathfindingNode endNode, PathfindingNode startNode)
|
||||
{
|
||||
var xDiff = endNode.TileRef.X - startNode.TileRef.X;
|
||||
var yDiff = endNode.TileRef.Y - startNode.TileRef.Y;
|
||||
|
||||
return RelativeDirection(xDiff, yDiff);
|
||||
}
|
||||
|
||||
public static Direction RelativeDirection(int x, int y)
|
||||
{
|
||||
switch (x)
|
||||
{
|
||||
case -1:
|
||||
switch (y)
|
||||
{
|
||||
case -1:
|
||||
return Direction.SouthWest;
|
||||
case 0:
|
||||
return Direction.West;
|
||||
case 1:
|
||||
return Direction.NorthWest;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
case 0:
|
||||
switch (y)
|
||||
{
|
||||
case -1:
|
||||
return Direction.South;
|
||||
case 0:
|
||||
throw new InvalidOperationException();
|
||||
case 1:
|
||||
return Direction.North;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
case 1:
|
||||
switch (y)
|
||||
{
|
||||
case -1:
|
||||
return Direction.SouthEast;
|
||||
case 0:
|
||||
return Direction.East;
|
||||
case 1:
|
||||
return Direction.NorthEast;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
317
Content.Server/NPC/Pathfinding/PathfindingNode.cs
Normal file
317
Content.Server/NPC/Pathfinding/PathfindingNode.cs
Normal file
@@ -0,0 +1,317 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Doors.Components;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.NPC.Pathfinding
|
||||
{
|
||||
public sealed class PathfindingNode
|
||||
{
|
||||
public PathfindingChunk ParentChunk => _parentChunk;
|
||||
private readonly PathfindingChunk _parentChunk;
|
||||
|
||||
public TileRef TileRef { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whenever there's a change in the collision layers we update the mask as the graph has more reads than writes
|
||||
/// </summary>
|
||||
public int BlockedCollisionMask { get; private set; }
|
||||
private readonly Dictionary<EntityUid, int> _blockedCollidables = new(0);
|
||||
|
||||
public IReadOnlyDictionary<EntityUid, int> PhysicsLayers => _physicsLayers;
|
||||
private readonly Dictionary<EntityUid, int> _physicsLayers = new(0);
|
||||
|
||||
/// <summary>
|
||||
/// The entities on this tile that require access to traverse
|
||||
/// </summary>
|
||||
/// We don't store the ICollection, at least for now, as we'd need to replicate the access code here
|
||||
public IReadOnlyCollection<AccessReaderComponent> AccessReaders => _accessReaders.Values;
|
||||
private readonly Dictionary<EntityUid, AccessReaderComponent> _accessReaders = new(0);
|
||||
|
||||
public PathfindingNode(PathfindingChunk parent, TileRef tileRef)
|
||||
{
|
||||
_parentChunk = parent;
|
||||
TileRef = tileRef;
|
||||
GenerateMask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return our neighboring nodes (even across chunks)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<PathfindingNode> GetNeighbors()
|
||||
{
|
||||
List<PathfindingChunk>? neighborChunks = null;
|
||||
if (ParentChunk.OnEdge(this))
|
||||
{
|
||||
neighborChunks = ParentChunk.RelevantChunks(this).ToList();
|
||||
}
|
||||
|
||||
for (var x = -1; x <= 1; x++)
|
||||
{
|
||||
for (var y = -1; y <= 1; y++)
|
||||
{
|
||||
if (x == 0 && y == 0) continue;
|
||||
var indices = new Vector2i(TileRef.X + x, TileRef.Y + y);
|
||||
if (ParentChunk.InBounds(indices))
|
||||
{
|
||||
var (relativeX, relativeY) = (indices.X - ParentChunk.Indices.X,
|
||||
indices.Y - ParentChunk.Indices.Y);
|
||||
yield return ParentChunk.Nodes[relativeX, relativeY];
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugTools.AssertNotNull(neighborChunks);
|
||||
// Get the relevant chunk and then get the node on it
|
||||
foreach (var neighbor in neighborChunks!)
|
||||
{
|
||||
// A lot of edge transitions are going to have a single neighboring chunk
|
||||
// (given > 1 only affects corners)
|
||||
// So we can just check the count to see if it's inbound
|
||||
if (neighborChunks.Count > 0 && !neighbor.InBounds(indices)) continue;
|
||||
var (relativeX, relativeY) = (indices.X - neighbor.Indices.X,
|
||||
indices.Y - neighbor.Indices.Y);
|
||||
yield return neighbor.Nodes[relativeX, relativeY];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PathfindingNode? GetNeighbor(Direction direction, IEntityManager? entManager = null)
|
||||
{
|
||||
var chunkXOffset = TileRef.X - ParentChunk.Indices.X;
|
||||
var chunkYOffset = TileRef.Y - ParentChunk.Indices.Y;
|
||||
Vector2i neighborVector2i;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case Direction.East:
|
||||
if (!ParentChunk.OnEdge(this))
|
||||
{
|
||||
return ParentChunk.Nodes[chunkXOffset + 1, chunkYOffset];
|
||||
}
|
||||
|
||||
neighborVector2i = new Vector2i(TileRef.X + 1, TileRef.Y);
|
||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
||||
{
|
||||
if (neighbor.InBounds(neighborVector2i))
|
||||
{
|
||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
||||
neighborVector2i.Y - neighbor.Indices.Y];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
case Direction.NorthEast:
|
||||
if (!ParentChunk.OnEdge(this))
|
||||
{
|
||||
return ParentChunk.Nodes[chunkXOffset + 1, chunkYOffset + 1];
|
||||
}
|
||||
|
||||
neighborVector2i = new Vector2i(TileRef.X + 1, TileRef.Y + 1);
|
||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
||||
{
|
||||
if (neighbor.InBounds(neighborVector2i))
|
||||
{
|
||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
||||
neighborVector2i.Y - neighbor.Indices.Y];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
case Direction.North:
|
||||
if (!ParentChunk.OnEdge(this))
|
||||
{
|
||||
return ParentChunk.Nodes[chunkXOffset, chunkYOffset + 1];
|
||||
}
|
||||
|
||||
neighborVector2i = new Vector2i(TileRef.X, TileRef.Y + 1);
|
||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
||||
{
|
||||
if (neighbor.InBounds(neighborVector2i))
|
||||
{
|
||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
||||
neighborVector2i.Y - neighbor.Indices.Y];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
case Direction.NorthWest:
|
||||
if (!ParentChunk.OnEdge(this))
|
||||
{
|
||||
return ParentChunk.Nodes[chunkXOffset - 1, chunkYOffset + 1];
|
||||
}
|
||||
|
||||
neighborVector2i = new Vector2i(TileRef.X - 1, TileRef.Y + 1);
|
||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
||||
{
|
||||
if (neighbor.InBounds(neighborVector2i))
|
||||
{
|
||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
||||
neighborVector2i.Y - neighbor.Indices.Y];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
case Direction.West:
|
||||
if (!ParentChunk.OnEdge(this))
|
||||
{
|
||||
return ParentChunk.Nodes[chunkXOffset - 1, chunkYOffset];
|
||||
}
|
||||
|
||||
neighborVector2i = new Vector2i(TileRef.X - 1, TileRef.Y);
|
||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
||||
{
|
||||
if (neighbor.InBounds(neighborVector2i))
|
||||
{
|
||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
||||
neighborVector2i.Y - neighbor.Indices.Y];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
case Direction.SouthWest:
|
||||
if (!ParentChunk.OnEdge(this))
|
||||
{
|
||||
return ParentChunk.Nodes[chunkXOffset - 1, chunkYOffset - 1];
|
||||
}
|
||||
|
||||
neighborVector2i = new Vector2i(TileRef.X - 1, TileRef.Y - 1);
|
||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
||||
{
|
||||
if (neighbor.InBounds(neighborVector2i))
|
||||
{
|
||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
||||
neighborVector2i.Y - neighbor.Indices.Y];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
case Direction.South:
|
||||
if (!ParentChunk.OnEdge(this))
|
||||
{
|
||||
return ParentChunk.Nodes[chunkXOffset, chunkYOffset - 1];
|
||||
}
|
||||
|
||||
neighborVector2i = new Vector2i(TileRef.X, TileRef.Y - 1);
|
||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
||||
{
|
||||
if (neighbor.InBounds(neighborVector2i))
|
||||
{
|
||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
||||
neighborVector2i.Y - neighbor.Indices.Y];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
case Direction.SouthEast:
|
||||
if (!ParentChunk.OnEdge(this))
|
||||
{
|
||||
return ParentChunk.Nodes[chunkXOffset + 1, chunkYOffset - 1];
|
||||
}
|
||||
|
||||
neighborVector2i = new Vector2i(TileRef.X + 1, TileRef.Y - 1);
|
||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
||||
{
|
||||
if (neighbor.InBounds(neighborVector2i))
|
||||
{
|
||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
||||
neighborVector2i.Y - neighbor.Indices.Y];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(direction), direction, null);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateTile(TileRef newTile)
|
||||
{
|
||||
TileRef = newTile;
|
||||
ParentChunk.Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call if this entity is relevant for the pathfinder
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// TODO: These 2 methods currently don't account for a bunch of changes (e.g. airlock unpowered, wrenching, etc.)
|
||||
/// TODO: Could probably optimise this slightly more.
|
||||
public void AddEntity(EntityUid entity, IPhysBody physicsComponent, IEntityManager? entMan = null)
|
||||
{
|
||||
IoCManager.Resolve(ref entMan);
|
||||
// If we're a door
|
||||
if (entMan.HasComponent<AirlockComponent>(entity) || entMan.HasComponent<DoorComponent>(entity))
|
||||
{
|
||||
// If we need access to traverse this then add to readers, otherwise no point adding it (except for maybe tile costs in future)
|
||||
// TODO: Check for powered I think (also need an event for when it's depowered
|
||||
// AccessReader calls this whenever opening / closing but it can seem to get called multiple times
|
||||
// Which may or may not be intended?
|
||||
if (entMan.TryGetComponent(entity, out AccessReaderComponent? accessReader) && !_accessReaders.ContainsKey(entity))
|
||||
{
|
||||
_accessReaders.TryAdd(entity, accessReader);
|
||||
ParentChunk.Dirty();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DebugTools.Assert((PathfindingSystem.TrackedCollisionLayers & physicsComponent.CollisionLayer) != 0);
|
||||
|
||||
if (physicsComponent.BodyType != BodyType.Static ||
|
||||
!physicsComponent.Hard)
|
||||
{
|
||||
_physicsLayers.TryAdd(entity, physicsComponent.CollisionLayer);
|
||||
}
|
||||
else
|
||||
{
|
||||
_blockedCollidables.TryAdd(entity, physicsComponent.CollisionLayer);
|
||||
GenerateMask();
|
||||
ParentChunk.Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the entity from this node.
|
||||
/// Will check each category and remove it from the applicable one
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
public void RemoveEntity(EntityUid entity)
|
||||
{
|
||||
// There's no guarantee that the entity isn't deleted
|
||||
// 90% of updates are probably entities moving around
|
||||
// Entity can't be under multiple categories so just checking each once is fine.
|
||||
if (_physicsLayers.Remove(entity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_accessReaders.Remove(entity))
|
||||
{
|
||||
ParentChunk.Dirty();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_blockedCollidables.Remove(entity))
|
||||
{
|
||||
GenerateMask();
|
||||
ParentChunk.Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateMask()
|
||||
{
|
||||
BlockedCollisionMask = 0x0;
|
||||
|
||||
foreach (var layer in _blockedCollidables.Values)
|
||||
{
|
||||
BlockedCollisionMask |= layer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
303
Content.Server/NPC/Pathfinding/PathfindingSystem.Grid.cs
Normal file
303
Content.Server/NPC/Pathfinding/PathfindingSystem.Grid.cs
Normal file
@@ -0,0 +1,303 @@
|
||||
using Content.Server.Access;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.NPC.Pathfinding;
|
||||
|
||||
public sealed partial class PathfindingSystem
|
||||
{
|
||||
/*
|
||||
* Handles pathfinding while on a grid.
|
||||
*/
|
||||
|
||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
// Queued pathfinding graph updates
|
||||
private readonly Queue<MoveEvent> _moveUpdateQueue = new();
|
||||
private readonly Queue<AccessReaderChangeEvent> _accessReaderUpdateQueue = new();
|
||||
private readonly Queue<TileRef> _tileUpdateQueue = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
|
||||
SubscribeLocalEvent<CollisionChangeEvent>(OnCollisionChange);
|
||||
SubscribeLocalEvent<MoveEvent>(OnMoveEvent);
|
||||
SubscribeLocalEvent<AccessReaderChangeEvent>(OnAccessChange);
|
||||
SubscribeLocalEvent<GridAddEvent>(OnGridAdd);
|
||||
SubscribeLocalEvent<TileChangedEvent>(OnTileChange);
|
||||
SubscribeLocalEvent<PhysicsBodyTypeChangedEvent>(OnBodyTypeChange);
|
||||
|
||||
// Handle all the base grid changes
|
||||
// Anything that affects traversal (i.e. collision layer) is handled separately.
|
||||
}
|
||||
|
||||
private void OnBodyTypeChange(ref PhysicsBodyTypeChangedEvent ev)
|
||||
{
|
||||
var xform = Transform(ev.Entity);
|
||||
|
||||
if (!IsRelevant(xform, ev.Component)) return;
|
||||
|
||||
var node = GetNode(xform);
|
||||
node?.RemoveEntity(ev.Entity);
|
||||
node?.AddEntity(ev.Entity, ev.Component, EntityManager);
|
||||
}
|
||||
|
||||
private void OnGridAdd(GridAddEvent ev)
|
||||
{
|
||||
EnsureComp<GridPathfindingComponent>(ev.EntityUid);
|
||||
}
|
||||
|
||||
private void OnCollisionChange(ref CollisionChangeEvent collisionEvent)
|
||||
{
|
||||
if (collisionEvent.CanCollide)
|
||||
{
|
||||
OnEntityAdd(collisionEvent.Body.Owner);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnEntityRemove(collisionEvent.Body.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMoveEvent(ref MoveEvent moveEvent)
|
||||
{
|
||||
_moveUpdateQueue.Enqueue(moveEvent);
|
||||
}
|
||||
|
||||
private void OnTileChange(TileChangedEvent ev)
|
||||
{
|
||||
_tileUpdateQueue.Enqueue(ev.NewTile);
|
||||
}
|
||||
|
||||
private void OnAccessChange(AccessReaderChangeEvent message)
|
||||
{
|
||||
_accessReaderUpdateQueue.Enqueue(message);
|
||||
}
|
||||
|
||||
private PathfindingChunk GetOrCreateChunk(TileRef tile)
|
||||
{
|
||||
var chunkX = (int) (Math.Floor((float) tile.X / PathfindingChunk.ChunkSize) * PathfindingChunk.ChunkSize);
|
||||
var chunkY = (int) (Math.Floor((float) tile.Y / PathfindingChunk.ChunkSize) * PathfindingChunk.ChunkSize);
|
||||
var vector2i = new Vector2i(chunkX, chunkY);
|
||||
var comp = Comp<GridPathfindingComponent>(tile.GridUid);
|
||||
var chunks = comp.Graph;
|
||||
|
||||
if (!chunks.TryGetValue(vector2i, out var chunk))
|
||||
{
|
||||
chunk = CreateChunk(comp, vector2i);
|
||||
}
|
||||
|
||||
return chunk;
|
||||
}
|
||||
|
||||
private PathfindingChunk CreateChunk(GridPathfindingComponent comp, Vector2i indices)
|
||||
{
|
||||
var grid = _mapManager.GetGrid(comp.Owner);
|
||||
var newChunk = new PathfindingChunk(grid.GridEntityId, indices);
|
||||
comp.Graph.Add(indices, newChunk);
|
||||
newChunk.Initialize(grid);
|
||||
|
||||
return newChunk;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Return the corresponding PathfindingNode for this tile
|
||||
/// </summary>
|
||||
/// <param name="tile"></param>
|
||||
/// <returns></returns>
|
||||
public PathfindingNode GetNode(TileRef tile)
|
||||
{
|
||||
var chunk = GetOrCreateChunk(tile);
|
||||
var node = chunk.GetNode(tile);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private void OnTileUpdate(TileRef tile)
|
||||
{
|
||||
if (!_mapManager.GridExists(tile.GridUid)) return;
|
||||
|
||||
var node = GetNode(tile);
|
||||
node.UpdateTile(tile);
|
||||
}
|
||||
|
||||
private bool IsRelevant(TransformComponent xform, PhysicsComponent physics)
|
||||
{
|
||||
return xform.GridUid != null && (TrackedCollisionLayers & physics.CollisionLayer) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add the entity to the relevant pathfinding node
|
||||
/// </summary>
|
||||
/// The node will filter it to the correct category (if possible)
|
||||
/// <param name="entity"></param>
|
||||
private void OnEntityAdd(EntityUid entity, TransformComponent? xform = null, PhysicsComponent? physics = null)
|
||||
{
|
||||
if (!Resolve(entity, ref xform, false) ||
|
||||
!Resolve(entity, ref physics, false)) return;
|
||||
|
||||
if (!IsRelevant(xform, physics) ||
|
||||
!_mapManager.TryGetGrid(xform.GridUid, out var grid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var tileRef = grid.GetTileRef(xform.Coordinates);
|
||||
|
||||
var chunk = GetOrCreateChunk(tileRef);
|
||||
var node = chunk.GetNode(tileRef);
|
||||
node.AddEntity(entity, physics, EntityManager);
|
||||
}
|
||||
|
||||
private void OnEntityRemove(EntityUid entity, TransformComponent? xform = null)
|
||||
{
|
||||
if (!Resolve(entity, ref xform, false) ||
|
||||
!_mapManager.TryGetGrid(xform.GridUid, out var grid)) return;
|
||||
|
||||
var node = GetNode(grid.GetTileRef(xform.Coordinates));
|
||||
node.RemoveEntity(entity);
|
||||
}
|
||||
|
||||
private void OnEntityRemove(EntityUid entity, EntityCoordinates coordinates)
|
||||
{
|
||||
var gridId = coordinates.GetGridUid(EntityManager);
|
||||
if (!_mapManager.TryGetGrid(gridId, out var grid)) return;
|
||||
|
||||
var node = GetNode(grid.GetTileRef(coordinates));
|
||||
node.RemoveEntity(entity);
|
||||
}
|
||||
|
||||
private PathfindingNode? GetNode(TransformComponent xform)
|
||||
{
|
||||
if (!_mapManager.TryGetGrid(xform.GridUid, out var grid)) return null;
|
||||
return GetNode(grid.GetTileRef(xform.Coordinates));
|
||||
}
|
||||
|
||||
private PathfindingNode? GetNode(EntityCoordinates coordinates)
|
||||
{
|
||||
if (!_mapManager.TryGetGrid(coordinates.GetGridUid(EntityManager), out var grid)) return null;
|
||||
return GetNode(grid.GetTileRef(coordinates));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When an entity moves around we'll remove it from its old node and add it to its new node (if applicable)
|
||||
/// </summary>
|
||||
/// <param name="moveEvent"></param>
|
||||
private void OnEntityMove(MoveEvent moveEvent)
|
||||
{
|
||||
if (!TryComp<TransformComponent>(moveEvent.Sender, out var xform)) return;
|
||||
|
||||
// If we've moved to space or the likes then remove us.
|
||||
if (!TryComp<PhysicsComponent>(moveEvent.Sender, out var physics) ||
|
||||
!IsRelevant(xform, physics))
|
||||
{
|
||||
OnEntityRemove(moveEvent.Sender, moveEvent.OldPosition);
|
||||
return;
|
||||
}
|
||||
|
||||
var oldNode = GetNode(moveEvent.OldPosition);
|
||||
var newNode = GetNode(moveEvent.NewPosition);
|
||||
|
||||
if (oldNode?.Equals(newNode) == true) return;
|
||||
|
||||
oldNode?.RemoveEntity(moveEvent.Sender);
|
||||
newNode?.AddEntity(moveEvent.Sender, physics, EntityManager);
|
||||
}
|
||||
|
||||
// TODO: Need to rethink the pathfinder utils (traversable etc.). Maybe just chuck them all in PathfindingSystem
|
||||
// Otherwise you get the steerer using this and the pathfinders using a different traversable.
|
||||
// Also look at increasing tile cost the more physics entities are on it
|
||||
public bool CanTraverse(EntityUid entity, EntityCoordinates coordinates)
|
||||
{
|
||||
var gridId = coordinates.GetGridUid(EntityManager);
|
||||
if (gridId == null)
|
||||
return false;
|
||||
var tile = _mapManager.GetGrid(gridId.Value).GetTileRef(coordinates);
|
||||
var node = GetNode(tile);
|
||||
return CanTraverse(entity, node);
|
||||
}
|
||||
|
||||
private bool CanTraverse(EntityUid entity, PathfindingNode node)
|
||||
{
|
||||
if (EntityManager.TryGetComponent(entity, out IPhysBody? physics) &&
|
||||
(physics.CollisionMask & node.BlockedCollisionMask) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var access = _accessReader.FindAccessTags(entity);
|
||||
foreach (var reader in node.AccessReaders)
|
||||
{
|
||||
if (!_accessReader.IsAllowed(access, reader))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnRoundRestart(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
_moveUpdateQueue.Clear();
|
||||
_accessReaderUpdateQueue.Clear();
|
||||
_tileUpdateQueue.Clear();
|
||||
}
|
||||
|
||||
private void ProcessGridUpdates()
|
||||
{
|
||||
var totalUpdates = 0;
|
||||
var bodyQuery = GetEntityQuery<PhysicsComponent>();
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
|
||||
foreach (var update in _accessReaderUpdateQueue)
|
||||
{
|
||||
if (!xformQuery.TryGetComponent(update.Sender, out var xform) ||
|
||||
!bodyQuery.TryGetComponent(update.Sender, out var body)) continue;
|
||||
|
||||
if (update.Enabled)
|
||||
{
|
||||
OnEntityAdd(update.Sender, xform, body);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnEntityRemove(update.Sender, xform);
|
||||
}
|
||||
|
||||
totalUpdates++;
|
||||
}
|
||||
|
||||
_accessReaderUpdateQueue.Clear();
|
||||
|
||||
foreach (var tile in _tileUpdateQueue)
|
||||
{
|
||||
OnTileUpdate(tile);
|
||||
totalUpdates++;
|
||||
}
|
||||
|
||||
_tileUpdateQueue.Clear();
|
||||
var moveUpdateCount = Math.Max(50 - totalUpdates, 0);
|
||||
|
||||
// Other updates are high priority so for this we'll just defer it if there's a spike (explosion, etc.)
|
||||
// If the move updates grow too large then we'll just do it
|
||||
if (_moveUpdateQueue.Count > 100)
|
||||
{
|
||||
moveUpdateCount = _moveUpdateQueue.Count - 100;
|
||||
}
|
||||
|
||||
moveUpdateCount = Math.Min(moveUpdateCount, _moveUpdateQueue.Count);
|
||||
|
||||
for (var i = 0; i < moveUpdateCount; i++)
|
||||
{
|
||||
OnEntityMove(_moveUpdateQueue.Dequeue());
|
||||
}
|
||||
|
||||
DebugTools.Assert(_moveUpdateQueue.Count < 1000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.NPC.Pathfinding;
|
||||
|
||||
public sealed partial class PathfindingSystem
|
||||
{
|
||||
// TODO: Re-use the existing simplifier. Because the pathfinding API sucks I just copy-pasted for now.
|
||||
public static List<TileRef> Simplify(List<TileRef> vertices, float tolerance = 0)
|
||||
{
|
||||
if (vertices.Count <= 3)
|
||||
return vertices;
|
||||
|
||||
var simplified = new List<TileRef>();
|
||||
|
||||
for (var i = 0; i < vertices.Count; i++)
|
||||
{
|
||||
// No wraparound for negative sooooo
|
||||
var prev = vertices[i == 0 ? vertices.Count - 1 : i - 1];
|
||||
var current = vertices[i];
|
||||
var next = vertices[(i + 1) % vertices.Count];
|
||||
|
||||
// If they collinear, continue
|
||||
if (IsCollinear(in prev, in current, in next, tolerance))
|
||||
continue;
|
||||
|
||||
simplified.Add(current);
|
||||
}
|
||||
|
||||
// Farseer didn't seem to handle straight lines and nuked all points
|
||||
if (simplified.Count == 0)
|
||||
{
|
||||
simplified.Add(vertices[0]);
|
||||
simplified.Add(vertices[^1]);
|
||||
}
|
||||
|
||||
return simplified;
|
||||
}
|
||||
|
||||
private static bool IsCollinear(in TileRef prev, in TileRef current, in TileRef next, float tolerance)
|
||||
{
|
||||
return FloatInRange(Area(in prev, in current, in next), -tolerance, tolerance);
|
||||
}
|
||||
|
||||
private static float Area(in TileRef a, in TileRef b, in TileRef c)
|
||||
{
|
||||
return a.X * (b.Y - c.Y) + b.X * (c.Y - a.Y) + c.X * (a.Y - b.Y);
|
||||
}
|
||||
|
||||
private static bool FloatInRange(float value, float min, float max)
|
||||
{
|
||||
return (value >= min && value <= max);
|
||||
}
|
||||
}
|
||||
78
Content.Server/NPC/Pathfinding/PathfindingSystem.cs
Normal file
78
Content.Server/NPC/Pathfinding/PathfindingSystem.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Threading;
|
||||
using Content.Server.CPUJob.JobQueues;
|
||||
using Content.Server.CPUJob.JobQueues.Queues;
|
||||
using Content.Server.NPC.Pathfinding.Pathfinders;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.NPC.Pathfinding
|
||||
{
|
||||
/// <summary>
|
||||
/// This system handles pathfinding graph updates as well as dispatches to the pathfinder
|
||||
/// (90% of what it's doing is graph updates so not much point splitting the 2 roles)
|
||||
/// </summary>
|
||||
public sealed partial class PathfindingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AccessReaderSystem _access = default!;
|
||||
|
||||
private readonly PathfindingJobQueue _pathfindingQueue = new();
|
||||
|
||||
public const int TrackedCollisionLayers = (int)
|
||||
(CollisionGroup.Impassable |
|
||||
CollisionGroup.MidImpassable |
|
||||
CollisionGroup.LowImpassable |
|
||||
CollisionGroup.HighImpassable);
|
||||
|
||||
/// <summary>
|
||||
/// Ask for the pathfinder to gimme somethin
|
||||
/// </summary>
|
||||
public Job<Queue<TileRef>> RequestPath(PathfindingArgs pathfindingArgs, CancellationToken cancellationToken)
|
||||
{
|
||||
var startNode = GetNode(pathfindingArgs.Start);
|
||||
var endNode = GetNode(pathfindingArgs.End);
|
||||
var job = new AStarPathfindingJob(0.001, startNode, endNode, pathfindingArgs, cancellationToken, EntityManager);
|
||||
_pathfindingQueue.EnqueueJob(job);
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
public Job<Queue<TileRef>> RequestPath(EntityUid source, EntityUid target, CancellationToken cancellationToken)
|
||||
{
|
||||
var collisionMask = 0;
|
||||
|
||||
if (TryComp<PhysicsComponent>(source, out var body))
|
||||
{
|
||||
collisionMask = body.CollisionMask;
|
||||
}
|
||||
|
||||
var start = TileRef.Zero;
|
||||
var end = TileRef.Zero;
|
||||
|
||||
if (TryComp<TransformComponent>(source, out var xform) &&
|
||||
_mapManager.TryGetGrid(xform.GridUid, out var grid) &&
|
||||
TryComp<TransformComponent>(target, out var targetXform) &&
|
||||
_mapManager.TryGetGrid(targetXform.GridUid, out var targetGrid))
|
||||
{
|
||||
start = grid.GetTileRef(xform.Coordinates);
|
||||
end = grid.GetTileRef(targetXform.Coordinates);
|
||||
}
|
||||
|
||||
var args = new PathfindingArgs(source, _access.FindAccessTags(source), collisionMask, start, end);
|
||||
|
||||
var startNode = GetNode(start);
|
||||
var endNode = GetNode(end);
|
||||
var job = new AStarPathfindingJob(0.001, startNode, endNode, args, cancellationToken, EntityManager);
|
||||
_pathfindingQueue.EnqueueJob(job);
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
ProcessGridUpdates();
|
||||
_pathfindingQueue.Process();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using Content.Server.NPC.Pathfinding.Pathfinders;
|
||||
using Content.Shared.AI;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.NPC.Pathfinding
|
||||
{
|
||||
#if DEBUG
|
||||
[UsedImplicitly]
|
||||
public sealed class ServerPathfindingDebugSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
AStarPathfindingJob.DebugRoute += DispatchAStarDebug;
|
||||
JpsPathfindingJob.DebugRoute += DispatchJpsDebug;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
AStarPathfindingJob.DebugRoute -= DispatchAStarDebug;
|
||||
JpsPathfindingJob.DebugRoute -= DispatchJpsDebug;
|
||||
}
|
||||
|
||||
private void DispatchAStarDebug(SharedAiDebug.AStarRouteDebug routeDebug)
|
||||
{
|
||||
var mapManager = IoCManager.Resolve<IMapManager>();
|
||||
var route = new List<Vector2>();
|
||||
foreach (var tile in routeDebug.Route)
|
||||
{
|
||||
var tileGrid = mapManager.GetGrid(tile.GridUid).GridTileToLocal(tile.GridIndices);
|
||||
route.Add(tileGrid.ToMapPos(EntityManager));
|
||||
}
|
||||
|
||||
var cameFrom = new Dictionary<Vector2, Vector2>();
|
||||
foreach (var (from, to) in routeDebug.CameFrom)
|
||||
{
|
||||
var tileOneGrid = mapManager.GetGrid(from.GridUid).GridTileToLocal(from.GridIndices);
|
||||
var tileOneWorld = tileOneGrid.ToMapPos(EntityManager);
|
||||
var tileTwoGrid = mapManager.GetGrid(to.GridUid).GridTileToLocal(to.GridIndices);
|
||||
var tileTwoWorld = tileTwoGrid.ToMapPos(EntityManager);
|
||||
cameFrom[tileOneWorld] = tileTwoWorld;
|
||||
}
|
||||
|
||||
var gScores = new Dictionary<Vector2, float>();
|
||||
foreach (var (tile, score) in routeDebug.GScores)
|
||||
{
|
||||
var tileGrid = mapManager.GetGrid(tile.GridUid).GridTileToLocal(tile.GridIndices);
|
||||
gScores[tileGrid.ToMapPos(EntityManager)] = score;
|
||||
}
|
||||
|
||||
var systemMessage = new SharedAiDebug.AStarRouteMessage(
|
||||
routeDebug.EntityUid,
|
||||
route,
|
||||
cameFrom,
|
||||
gScores,
|
||||
routeDebug.TimeTaken
|
||||
);
|
||||
|
||||
RaiseNetworkEvent(systemMessage);
|
||||
}
|
||||
|
||||
private void DispatchJpsDebug(SharedAiDebug.JpsRouteDebug routeDebug)
|
||||
{
|
||||
var mapManager = IoCManager.Resolve<IMapManager>();
|
||||
var route = new List<Vector2>();
|
||||
foreach (var tile in routeDebug.Route)
|
||||
{
|
||||
var tileGrid = mapManager.GetGrid(tile.GridUid).GridTileToLocal(tile.GridIndices);
|
||||
route.Add(tileGrid.ToMapPos(EntityManager));
|
||||
}
|
||||
|
||||
var jumpNodes = new List<Vector2>();
|
||||
foreach (var tile in routeDebug.JumpNodes)
|
||||
{
|
||||
var tileGrid = mapManager.GetGrid(tile.GridUid).GridTileToLocal(tile.GridIndices);
|
||||
jumpNodes.Add(tileGrid.ToMapPos(EntityManager));
|
||||
}
|
||||
|
||||
var systemMessage = new SharedAiDebug.JpsRouteMessage(
|
||||
routeDebug.EntityUid,
|
||||
route,
|
||||
jumpNodes,
|
||||
routeDebug.TimeTaken
|
||||
);
|
||||
|
||||
RaiseNetworkEvent(systemMessage);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Reference in New Issue
Block a user