Pathfinder hotfixes (#8201)
This commit is contained in:
@@ -335,7 +335,7 @@ namespace Content.Client.AI
|
|||||||
{
|
{
|
||||||
Regions[gridId][chunk].Add(region, nodes);
|
Regions[gridId][chunk].Add(region, nodes);
|
||||||
_regionColors[gridId][chunk][region] = new Color(robustRandom.NextFloat(), robustRandom.NextFloat(),
|
_regionColors[gridId][chunk][region] = new Color(robustRandom.NextFloat(), robustRandom.NextFloat(),
|
||||||
robustRandom.NextFloat(), 0.3f);
|
robustRandom.NextFloat(), 0.5f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.AI.Pathfinding;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Maths;
|
|
||||||
|
|
||||||
namespace Content.IntegrationTests.Tests.Pathfinding
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
[TestOf(typeof(PathfindingChunk))]
|
|
||||||
public sealed class PathfindingChunkTest : ContentIntegrationTest
|
|
||||||
{
|
|
||||||
[Test]
|
|
||||||
public async Task Test()
|
|
||||||
{
|
|
||||||
var server = StartServer();
|
|
||||||
|
|
||||||
server.Assert(() =>
|
|
||||||
{
|
|
||||||
var pathfindingSystem = EntitySystem.Get<PathfindingSystem>();
|
|
||||||
var mapMan = IoCManager.Resolve<IMapManager>();
|
|
||||||
|
|
||||||
// Setup
|
|
||||||
var grid = GetMainGrid(mapMan);
|
|
||||||
var chunkTile = grid.GetTileRef(new Vector2i(0, 0));
|
|
||||||
var chunk = pathfindingSystem.GetChunk(chunkTile);
|
|
||||||
Assert.That(chunk.Nodes.Length == PathfindingChunk.ChunkSize * PathfindingChunk.ChunkSize);
|
|
||||||
|
|
||||||
// Neighbors
|
|
||||||
var chunkNeighbors = chunk.GetNeighbors().ToList();
|
|
||||||
Assert.That(chunkNeighbors.Count == 0);
|
|
||||||
var neighborChunkTile = grid.GetTileRef(new Vector2i(PathfindingChunk.ChunkSize, PathfindingChunk.ChunkSize));
|
|
||||||
var neighborChunk = pathfindingSystem.GetChunk(neighborChunkTile);
|
|
||||||
chunkNeighbors = chunk.GetNeighbors().ToList();
|
|
||||||
Assert.That(chunkNeighbors.Count == 1);
|
|
||||||
|
|
||||||
// Directions
|
|
||||||
Assert.That(PathfindingHelpers.RelativeDirection(neighborChunk, chunk) == Direction.NorthEast);
|
|
||||||
Assert.That(PathfindingHelpers.RelativeDirection(chunk.Nodes[0, 1], chunk.Nodes[0, 0]) == Direction.North);
|
|
||||||
|
|
||||||
// Nodes
|
|
||||||
var node = chunk.Nodes[1, 1];
|
|
||||||
var nodeNeighbors = node.GetNeighbors().ToList();
|
|
||||||
Assert.That(nodeNeighbors.Count == 8);
|
|
||||||
|
|
||||||
// Bottom-left corner with no chunk neighbor
|
|
||||||
node = chunk.Nodes[0, 0];
|
|
||||||
nodeNeighbors = node.GetNeighbors().ToList();
|
|
||||||
Assert.That(nodeNeighbors.Count == 3);
|
|
||||||
|
|
||||||
// Given we have 1 NE neighbor then NE corner should have 4 neighbors due to the 1 extra from the neighbor chunk
|
|
||||||
node = chunk.Nodes[PathfindingChunk.ChunkSize - 1, PathfindingChunk.ChunkSize - 1];
|
|
||||||
nodeNeighbors = node.GetNeighbors().ToList();
|
|
||||||
Assert.That(nodeNeighbors.Count == 4);
|
|
||||||
});
|
|
||||||
await server.WaitIdleAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Content.Server.AI.Pathfinding;
|
||||||
|
|
||||||
|
[RegisterComponent]
|
||||||
|
[Friend(typeof(PathfindingSystem))]
|
||||||
|
public sealed class GridPathfindingComponent : Component, IPathfindingGraph
|
||||||
|
{
|
||||||
|
public Dictionary<Vector2i, PathfindingChunk> Graph = new();
|
||||||
|
}
|
||||||
6
Content.Server/AI/Pathfinding/IPathfindingGraph.cs
Normal file
6
Content.Server/AI/Pathfinding/IPathfindingGraph.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Content.Server.AI.Pathfinding;
|
||||||
|
|
||||||
|
public interface IPathfindingGraph
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ namespace Content.Server.AI.Pathfinding
|
|||||||
public sealed class PathfindingChunk
|
public sealed class PathfindingChunk
|
||||||
{
|
{
|
||||||
public TimeSpan LastUpdate { get; private set; }
|
public TimeSpan LastUpdate { get; private set; }
|
||||||
public GridId GridId { get; }
|
public EntityUid GridId { get; }
|
||||||
|
|
||||||
public Vector2i Indices => _indices;
|
public Vector2i Indices => _indices;
|
||||||
private readonly Vector2i _indices;
|
private readonly Vector2i _indices;
|
||||||
@@ -27,7 +27,7 @@ namespace Content.Server.AI.Pathfinding
|
|||||||
public PathfindingNode[,] Nodes => _nodes;
|
public PathfindingNode[,] Nodes => _nodes;
|
||||||
private readonly PathfindingNode[,] _nodes = new PathfindingNode[ChunkSize,ChunkSize];
|
private readonly PathfindingNode[,] _nodes = new PathfindingNode[ChunkSize,ChunkSize];
|
||||||
|
|
||||||
public PathfindingChunk(GridId gridId, Vector2i indices)
|
public PathfindingChunk(EntityUid gridId, Vector2i indices)
|
||||||
{
|
{
|
||||||
GridId = gridId;
|
GridId = gridId;
|
||||||
_indices = indices;
|
_indices = indices;
|
||||||
@@ -57,10 +57,10 @@ namespace Content.Server.AI.Pathfinding
|
|||||||
.RaiseEvent(EventSource.Local, new PathfindingChunkUpdateMessage(this));
|
.RaiseEvent(EventSource.Local, new PathfindingChunkUpdateMessage(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<PathfindingChunk> GetNeighbors()
|
public IEnumerable<PathfindingChunk> GetNeighbors(IEntityManager? entManager = null)
|
||||||
{
|
{
|
||||||
var pathfindingSystem = EntitySystem.Get<PathfindingSystem>();
|
IoCManager.Resolve(ref entManager);
|
||||||
var chunkGrid = pathfindingSystem.Graph[GridId];
|
var chunkGrid = entManager.GetComponent<GridPathfindingComponent>(GridId).Graph;
|
||||||
|
|
||||||
for (var x = -1; x <= 1; x++)
|
for (var x = -1; x <= 1; x++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,17 +38,6 @@ namespace Content.Server.AI.Pathfinding
|
|||||||
GenerateMask();
|
GenerateMask();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsRelevant(EntityUid entity, IPhysBody physicsComponent)
|
|
||||||
{
|
|
||||||
if (IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(entity).GridID == GridId.Invalid ||
|
|
||||||
(PathfindingSystem.TrackedCollisionLayers & physicsComponent.CollisionLayer) == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Return our neighboring nodes (even across chunks)
|
/// Return our neighboring nodes (even across chunks)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -93,7 +82,7 @@ namespace Content.Server.AI.Pathfinding
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PathfindingNode? GetNeighbor(Direction direction)
|
public PathfindingNode? GetNeighbor(Direction direction, IEntityManager? entManager = null)
|
||||||
{
|
{
|
||||||
var chunkXOffset = TileRef.X - ParentChunk.Indices.X;
|
var chunkXOffset = TileRef.X - ParentChunk.Indices.X;
|
||||||
var chunkYOffset = TileRef.Y - ParentChunk.Indices.Y;
|
var chunkYOffset = TileRef.Y - ParentChunk.Indices.Y;
|
||||||
@@ -254,9 +243,9 @@ namespace Content.Server.AI.Pathfinding
|
|||||||
/// <param name="entity"></param>
|
/// <param name="entity"></param>
|
||||||
/// TODO: These 2 methods currently don't account for a bunch of changes (e.g. airlock unpowered, wrenching, etc.)
|
/// 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.
|
/// TODO: Could probably optimise this slightly more.
|
||||||
public void AddEntity(EntityUid entity, IPhysBody physicsComponent)
|
public void AddEntity(EntityUid entity, IPhysBody physicsComponent, IEntityManager? entMan = null)
|
||||||
{
|
{
|
||||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
IoCManager.Resolve(ref entMan);
|
||||||
// If we're a door
|
// If we're a door
|
||||||
if (entMan.HasComponent<AirlockComponent>(entity) || entMan.HasComponent<DoorComponent>(entity))
|
if (entMan.HasComponent<AirlockComponent>(entity) || entMan.HasComponent<DoorComponent>(entity))
|
||||||
{
|
{
|
||||||
|
|||||||
273
Content.Server/AI/Pathfinding/PathfindingSystem.Grid.cs
Normal file
273
Content.Server/AI/Pathfinding/PathfindingSystem.Grid.cs
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
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.AI.Pathfinding;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles pathfinding while on a grid.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class PathfindingSystem
|
||||||
|
{
|
||||||
|
[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);
|
||||||
|
|
||||||
|
// Handle all the base grid changes
|
||||||
|
// Anything that affects traversal (i.e. collision layer) is handled separately.
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Index, 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.GridIndex)) return;
|
||||||
|
|
||||||
|
var node = GetNode(tile);
|
||||||
|
node.UpdateTile(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsRelevant(TransformComponent xform, PhysicsComponent physics)
|
||||||
|
{
|
||||||
|
return xform.GridID != GridId.Invalid && (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.GridID, 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.GridID, out var grid)) return;
|
||||||
|
|
||||||
|
var node = GetNode(grid.GetTileRef(xform.Coordinates));
|
||||||
|
node.RemoveEntity(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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) ||
|
||||||
|
moveEvent.NewPosition.GetGridId(EntityManager) == GridId.Invalid)
|
||||||
|
{
|
||||||
|
OnEntityRemove(moveEvent.Sender, xform);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldGridId = moveEvent.OldPosition.GetGridId(EntityManager);
|
||||||
|
var gridId = moveEvent.NewPosition.GetGridId(EntityManager);
|
||||||
|
|
||||||
|
if (_mapManager.TryGetGrid(oldGridId, out var oldGrid))
|
||||||
|
{
|
||||||
|
var oldNode = GetNode(oldGrid.GetTileRef(moveEvent.OldPosition));
|
||||||
|
oldNode.RemoveEntity(moveEvent.Sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_mapManager.TryGetGrid(gridId, out var grid))
|
||||||
|
{
|
||||||
|
var newNode = GetNode(grid.GetTileRef(moveEvent.OldPosition));
|
||||||
|
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.GetGridId(EntityManager);
|
||||||
|
var tile = _mapManager.GetGrid(gridId).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(reader, access))
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,35 +12,14 @@ using Robust.Shared.Utility;
|
|||||||
|
|
||||||
namespace Content.Server.AI.Pathfinding
|
namespace Content.Server.AI.Pathfinding
|
||||||
{
|
{
|
||||||
/*
|
|
||||||
// TODO: IMO use rectangular symmetry reduction on the nodes with collision at all. (currently planned to be implemented via AiReachableSystem and expanded later).
|
|
||||||
alternatively store all rooms and have an alternative graph for humanoid mobs (same collision mask, needs access etc). You could also just path from room to room as needed.
|
|
||||||
// TODO: Longer term -> Handle collision layer changes?
|
|
||||||
TODO: Handle container entities so they're not tracked.
|
|
||||||
*/
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This system handles pathfinding graph updates as well as dispatches to the pathfinder
|
/// 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)
|
/// (90% of what it's doing is graph updates so not much point splitting the 2 roles)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PathfindingSystem : EntitySystem
|
public sealed partial class PathfindingSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
|
||||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<GridId, Dictionary<Vector2i, PathfindingChunk>> Graph => _graph;
|
|
||||||
private readonly Dictionary<GridId, Dictionary<Vector2i, PathfindingChunk>> _graph = new();
|
|
||||||
|
|
||||||
private readonly PathfindingJobQueue _pathfindingQueue = new();
|
private readonly PathfindingJobQueue _pathfindingQueue = new();
|
||||||
|
|
||||||
// Queued pathfinding graph updates
|
|
||||||
private readonly Queue<CollisionChangeMessage> _collidableUpdateQueue = new();
|
|
||||||
private readonly Queue<MoveEvent> _moveUpdateQueue = new();
|
|
||||||
private readonly Queue<AccessReaderChangeMessage> _accessReaderUpdateQueue = new();
|
|
||||||
private readonly Queue<TileRef> _tileUpdateQueue = new();
|
|
||||||
|
|
||||||
// Need to store previously known entity positions for collidables for when they move
|
|
||||||
private readonly Dictionary<EntityUid, PathfindingNode> _lastKnownPositions = new();
|
|
||||||
|
|
||||||
public const int TrackedCollisionLayers = (int)
|
public const int TrackedCollisionLayers = (int)
|
||||||
(CollisionGroup.Impassable |
|
(CollisionGroup.Impassable |
|
||||||
CollisionGroup.MidImpassable |
|
CollisionGroup.MidImpassable |
|
||||||
@@ -66,315 +45,8 @@ namespace Content.Server.AI.Pathfinding
|
|||||||
public override void Update(float frameTime)
|
public override void Update(float frameTime)
|
||||||
{
|
{
|
||||||
base.Update(frameTime);
|
base.Update(frameTime);
|
||||||
|
ProcessGridUpdates();
|
||||||
// Make sure graph is updated, then get pathfinders
|
|
||||||
ProcessGraphUpdates();
|
|
||||||
_pathfindingQueue.Process();
|
_pathfindingQueue.Process();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessGraphUpdates()
|
|
||||||
{
|
|
||||||
var totalUpdates = 0;
|
|
||||||
|
|
||||||
foreach (var update in _collidableUpdateQueue)
|
|
||||||
{
|
|
||||||
if (!EntityManager.EntityExists(update.Owner)) continue;
|
|
||||||
|
|
||||||
if (update.CanCollide)
|
|
||||||
{
|
|
||||||
HandleEntityAdd(update.Owner);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HandleEntityRemove(update.Owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
totalUpdates++;
|
|
||||||
}
|
|
||||||
|
|
||||||
_collidableUpdateQueue.Clear();
|
|
||||||
|
|
||||||
foreach (var update in _accessReaderUpdateQueue)
|
|
||||||
{
|
|
||||||
if (update.Enabled)
|
|
||||||
{
|
|
||||||
HandleEntityAdd(update.Sender);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HandleEntityRemove(update.Sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
totalUpdates++;
|
|
||||||
}
|
|
||||||
|
|
||||||
_accessReaderUpdateQueue.Clear();
|
|
||||||
|
|
||||||
foreach (var tile in _tileUpdateQueue)
|
|
||||||
{
|
|
||||||
HandleTileUpdate(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++)
|
|
||||||
{
|
|
||||||
HandleEntityMove(_moveUpdateQueue.Dequeue());
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugTools.Assert(_moveUpdateQueue.Count < 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PathfindingChunk GetChunk(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);
|
|
||||||
|
|
||||||
if (_graph.TryGetValue(tile.GridIndex, out var chunks))
|
|
||||||
{
|
|
||||||
if (!chunks.ContainsKey(vector2i))
|
|
||||||
{
|
|
||||||
CreateChunk(tile.GridIndex, vector2i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return chunks[vector2i];
|
|
||||||
}
|
|
||||||
|
|
||||||
var newChunk = CreateChunk(tile.GridIndex, vector2i);
|
|
||||||
return newChunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PathfindingChunk CreateChunk(GridId gridId, Vector2i indices)
|
|
||||||
{
|
|
||||||
var newChunk = new PathfindingChunk(gridId, indices);
|
|
||||||
if (!_graph.ContainsKey(gridId))
|
|
||||||
{
|
|
||||||
_graph.Add(gridId, new Dictionary<Vector2i, PathfindingChunk>());
|
|
||||||
}
|
|
||||||
|
|
||||||
_graph[gridId].Add(indices, newChunk);
|
|
||||||
newChunk.Initialize(_mapManager.GetGrid(gridId));
|
|
||||||
|
|
||||||
return newChunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the entity's tile position, then get the corresponding node
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entity"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public PathfindingNode GetNode(EntityUid entity)
|
|
||||||
{
|
|
||||||
var tile = _mapManager.GetGrid(EntityManager.GetComponent<TransformComponent>(entity).GridID).GetTileRef(EntityManager.GetComponent<TransformComponent>(entity).Coordinates);
|
|
||||||
return GetNode(tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return the corresponding PathfindingNode for this tile
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="tile"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public PathfindingNode GetNode(TileRef tile)
|
|
||||||
{
|
|
||||||
var chunk = GetChunk(tile);
|
|
||||||
var node = chunk.GetNode(tile);
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
|
||||||
SubscribeLocalEvent<CollisionChangeMessage>(QueueCollisionChangeMessage);
|
|
||||||
SubscribeLocalEvent<MoveEvent>(QueueMoveEvent);
|
|
||||||
SubscribeLocalEvent<AccessReaderChangeMessage>(QueueAccessChangeMessage);
|
|
||||||
SubscribeLocalEvent<GridRemovalEvent>(HandleGridRemoval);
|
|
||||||
SubscribeLocalEvent<TileChangedEvent>(QueueTileChange);
|
|
||||||
|
|
||||||
// Handle all the base grid changes
|
|
||||||
// Anything that affects traversal (i.e. collision layer) is handled separately.
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleTileUpdate(TileRef tile)
|
|
||||||
{
|
|
||||||
if (!_mapManager.GridExists(tile.GridIndex)) return;
|
|
||||||
|
|
||||||
var node = GetNode(tile);
|
|
||||||
node.UpdateTile(tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleGridRemoval(GridRemovalEvent ev)
|
|
||||||
{
|
|
||||||
if (_graph.ContainsKey(ev.GridId))
|
|
||||||
{
|
|
||||||
_graph.Remove(ev.GridId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void QueueTileChange(TileChangedEvent ev)
|
|
||||||
{
|
|
||||||
_tileUpdateQueue.Enqueue(ev.NewTile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void QueueAccessChangeMessage(AccessReaderChangeMessage message)
|
|
||||||
{
|
|
||||||
_accessReaderUpdateQueue.Enqueue(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 HandleEntityAdd(EntityUid entity)
|
|
||||||
{
|
|
||||||
if (Deleted(entity) ||
|
|
||||||
_lastKnownPositions.ContainsKey(entity) ||
|
|
||||||
!EntityManager.TryGetComponent(entity, out IPhysBody? physics) ||
|
|
||||||
!PathfindingNode.IsRelevant(entity, physics))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var xform = EntityManager.GetComponent<TransformComponent>(entity);
|
|
||||||
var grid = _mapManager.GetGrid(xform.GridID);
|
|
||||||
var tileRef = grid.GetTileRef(xform.Coordinates);
|
|
||||||
|
|
||||||
var chunk = GetChunk(tileRef);
|
|
||||||
var node = chunk.GetNode(tileRef);
|
|
||||||
node.AddEntity(entity, physics);
|
|
||||||
_lastKnownPositions.Add(entity, node);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleEntityRemove(EntityUid entity)
|
|
||||||
{
|
|
||||||
if (!_lastKnownPositions.TryGetValue(entity, out var node))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
node.RemoveEntity(entity);
|
|
||||||
_lastKnownPositions.Remove(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void QueueMoveEvent(ref MoveEvent moveEvent)
|
|
||||||
{
|
|
||||||
_moveUpdateQueue.Enqueue(moveEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 HandleEntityMove(MoveEvent moveEvent)
|
|
||||||
{
|
|
||||||
// If we've moved to space or the likes then remove us.
|
|
||||||
if ((!EntityManager.EntityExists(moveEvent.Sender) ? EntityLifeStage.Deleted : EntityManager.GetComponent<MetaDataComponent>(moveEvent.Sender).EntityLifeStage) >= EntityLifeStage.Deleted ||
|
|
||||||
!EntityManager.TryGetComponent(moveEvent.Sender, out IPhysBody? physics) ||
|
|
||||||
!PathfindingNode.IsRelevant(moveEvent.Sender, physics) ||
|
|
||||||
moveEvent.NewPosition.GetGridId(EntityManager) == GridId.Invalid)
|
|
||||||
{
|
|
||||||
HandleEntityRemove(moveEvent.Sender);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Memory leak protection until grid parenting confirmed fix / you REALLY need the performance
|
|
||||||
var xform = EntityManager.GetComponent<TransformComponent>(moveEvent.Sender);
|
|
||||||
var gridBounds = _mapManager.GetGrid(xform.GridID).WorldBounds;
|
|
||||||
|
|
||||||
if (!gridBounds.Contains(xform.WorldPosition))
|
|
||||||
{
|
|
||||||
HandleEntityRemove(moveEvent.Sender);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we move from space to a grid we may need to start tracking it.
|
|
||||||
if (!_lastKnownPositions.TryGetValue(moveEvent.Sender, out var oldNode))
|
|
||||||
{
|
|
||||||
HandleEntityAdd(moveEvent.Sender);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newGridId = moveEvent.NewPosition.GetGridId(EntityManager);
|
|
||||||
if (newGridId == GridId.Invalid)
|
|
||||||
{
|
|
||||||
HandleEntityRemove(moveEvent.Sender);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The pathfinding graph is tile-based so first we'll check if they're on a different tile and if we need to update.
|
|
||||||
// If you get entities bigger than 1 tile wide you'll need some other system so god help you.
|
|
||||||
var newTile = _mapManager.GetGrid(newGridId).GetTileRef(moveEvent.NewPosition);
|
|
||||||
|
|
||||||
if (oldNode == null || oldNode.TileRef == newTile)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newNode = GetNode(newTile);
|
|
||||||
_lastKnownPositions[moveEvent.Sender] = newNode;
|
|
||||||
|
|
||||||
oldNode.RemoveEntity(moveEvent.Sender);
|
|
||||||
newNode.AddEntity(moveEvent.Sender, physics);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void QueueCollisionChangeMessage(CollisionChangeMessage collisionMessage)
|
|
||||||
{
|
|
||||||
_collidableUpdateQueue.Enqueue(collisionMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.GetGridId(EntityManager);
|
|
||||||
var tile = _mapManager.GetGrid(gridId).GetTileRef(coordinates);
|
|
||||||
var node = GetNode(tile);
|
|
||||||
return CanTraverse(entity, node);
|
|
||||||
}
|
|
||||||
|
|
||||||
public 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(reader, access))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reset(RoundRestartCleanupEvent ev)
|
|
||||||
{
|
|
||||||
_graph.Clear();
|
|
||||||
_collidableUpdateQueue.Clear();
|
|
||||||
_moveUpdateQueue.Clear();
|
|
||||||
_accessReaderUpdateQueue.Clear();
|
|
||||||
_tileUpdateQueue.Clear();
|
|
||||||
_lastKnownPositions.Clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ namespace Content.Server.AI.Pathfinding
|
|||||||
base.Initialize();
|
base.Initialize();
|
||||||
AStarPathfindingJob.DebugRoute += DispatchAStarDebug;
|
AStarPathfindingJob.DebugRoute += DispatchAStarDebug;
|
||||||
JpsPathfindingJob.DebugRoute += DispatchJpsDebug;
|
JpsPathfindingJob.DebugRoute += DispatchJpsDebug;
|
||||||
SubscribeNetworkEvent<SharedAiDebug.RequestPathfindingGraphMessage>(DispatchGraph);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Shutdown()
|
public override void Shutdown()
|
||||||
@@ -88,37 +87,6 @@ namespace Content.Server.AI.Pathfinding
|
|||||||
|
|
||||||
RaiseNetworkEvent(systemMessage);
|
RaiseNetworkEvent(systemMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DispatchGraph(SharedAiDebug.RequestPathfindingGraphMessage message)
|
|
||||||
{
|
|
||||||
var pathfindingSystem = EntityManager.EntitySysManager.GetEntitySystem<PathfindingSystem>();
|
|
||||||
var mapManager = IoCManager.Resolve<IMapManager>();
|
|
||||||
var result = new Dictionary<int, List<Vector2>>();
|
|
||||||
|
|
||||||
var idx = 0;
|
|
||||||
|
|
||||||
foreach (var (gridId, chunks) in pathfindingSystem.Graph)
|
|
||||||
{
|
|
||||||
var gridManager = mapManager.GetGrid(gridId);
|
|
||||||
|
|
||||||
foreach (var chunk in chunks.Values)
|
|
||||||
{
|
|
||||||
var nodes = new List<Vector2>();
|
|
||||||
foreach (var node in chunk.Nodes)
|
|
||||||
{
|
|
||||||
var worldTile = gridManager.GridTileToWorldPos(node.TileRef.GridIndices);
|
|
||||||
|
|
||||||
nodes.Add(worldTile);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Add(idx, nodes);
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var systemMessage = new SharedAiDebug.PathfindingGraphMessage(result);
|
|
||||||
RaiseNetworkEvent(systemMessage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -387,19 +387,6 @@ namespace Content.Server.AI.Steering
|
|||||||
movementVector += CollisionAvoidance(entity, movementVector, ignoredCollision);
|
movementVector += CollisionAvoidance(entity, movementVector, ignoredCollision);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Jesus this code is shit, slork is a cute dork, but the pathfinder should annotate this.
|
|
||||||
if (_mapManager.TryGetGrid(nextGrid.Value.EntityId, out var grid))
|
|
||||||
{
|
|
||||||
foreach (var ent in grid.GetAnchoredEntities(nextGrid.Value))
|
|
||||||
{
|
|
||||||
if (HasComp<DoorComponent>(ent))
|
|
||||||
{
|
|
||||||
_interactionSystem.InteractHand(entity, ent);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group behaviors would also go here e.g. separation, cohesion, alignment
|
// Group behaviors would also go here e.g. separation, cohesion, alignment
|
||||||
|
|
||||||
// Move towards it
|
// Move towards it
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
namespace Content.Server.Access
|
namespace Content.Server.Access
|
||||||
{
|
{
|
||||||
public sealed class AccessReaderChangeMessage : EntityEventArgs
|
public sealed class AccessReaderChangeEvent : EntityEventArgs
|
||||||
{
|
{
|
||||||
public EntityUid Sender { get; }
|
public EntityUid Sender { get; }
|
||||||
|
|
||||||
public bool Enabled { get; }
|
public bool Enabled { get; }
|
||||||
|
|
||||||
public AccessReaderChangeMessage(EntityUid entity, bool enabled)
|
public AccessReaderChangeEvent(EntityUid entity, bool enabled)
|
||||||
{
|
{
|
||||||
Sender = entity;
|
Sender = entity;
|
||||||
Enabled = enabled;
|
Enabled = enabled;
|
||||||
@@ -67,7 +67,7 @@ public sealed class DoorSystem : SharedDoorSystem
|
|||||||
_airtightSystem.SetAirblocked(airtight, collidable);
|
_airtightSystem.SetAirblocked(airtight, collidable);
|
||||||
|
|
||||||
// Pathfinding / AI stuff.
|
// Pathfinding / AI stuff.
|
||||||
RaiseLocalEvent(new AccessReaderChangeMessage(uid, collidable));
|
RaiseLocalEvent(new AccessReaderChangeEvent(uid, collidable));
|
||||||
|
|
||||||
base.SetCollidable(uid, collidable, door, physics, occluder);
|
base.SetCollidable(uid, collidable, door, physics, occluder);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user