Pathfinder hotfixes (#8201)
This commit is contained in:
@@ -335,7 +335,7 @@ namespace Content.Client.AI
|
||||
{
|
||||
Regions[gridId][chunk].Add(region, nodes);
|
||||
_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 TimeSpan LastUpdate { get; private set; }
|
||||
public GridId GridId { get; }
|
||||
public EntityUid GridId { get; }
|
||||
|
||||
public Vector2i Indices => _indices;
|
||||
private readonly Vector2i _indices;
|
||||
@@ -27,7 +27,7 @@ namespace Content.Server.AI.Pathfinding
|
||||
public PathfindingNode[,] Nodes => _nodes;
|
||||
private readonly PathfindingNode[,] _nodes = new PathfindingNode[ChunkSize,ChunkSize];
|
||||
|
||||
public PathfindingChunk(GridId gridId, Vector2i indices)
|
||||
public PathfindingChunk(EntityUid gridId, Vector2i indices)
|
||||
{
|
||||
GridId = gridId;
|
||||
_indices = indices;
|
||||
@@ -57,10 +57,10 @@ namespace Content.Server.AI.Pathfinding
|
||||
.RaiseEvent(EventSource.Local, new PathfindingChunkUpdateMessage(this));
|
||||
}
|
||||
|
||||
public IEnumerable<PathfindingChunk> GetNeighbors()
|
||||
public IEnumerable<PathfindingChunk> GetNeighbors(IEntityManager? entManager = null)
|
||||
{
|
||||
var pathfindingSystem = EntitySystem.Get<PathfindingSystem>();
|
||||
var chunkGrid = pathfindingSystem.Graph[GridId];
|
||||
IoCManager.Resolve(ref entManager);
|
||||
var chunkGrid = entManager.GetComponent<GridPathfindingComponent>(GridId).Graph;
|
||||
|
||||
for (var x = -1; x <= 1; x++)
|
||||
{
|
||||
|
||||
@@ -38,17 +38,6 @@ namespace Content.Server.AI.Pathfinding
|
||||
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>
|
||||
/// Return our neighboring nodes (even across chunks)
|
||||
/// </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 chunkYOffset = TileRef.Y - ParentChunk.Indices.Y;
|
||||
@@ -254,9 +243,9 @@ namespace Content.Server.AI.Pathfinding
|
||||
/// <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)
|
||||
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 (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
|
||||
{
|
||||
/*
|
||||
// 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>
|
||||
/// 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 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();
|
||||
|
||||
// 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)
|
||||
(CollisionGroup.Impassable |
|
||||
CollisionGroup.MidImpassable |
|
||||
@@ -66,315 +45,8 @@ namespace Content.Server.AI.Pathfinding
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
// Make sure graph is updated, then get pathfinders
|
||||
ProcessGraphUpdates();
|
||||
ProcessGridUpdates();
|
||||
_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();
|
||||
AStarPathfindingJob.DebugRoute += DispatchAStarDebug;
|
||||
JpsPathfindingJob.DebugRoute += DispatchJpsDebug;
|
||||
SubscribeNetworkEvent<SharedAiDebug.RequestPathfindingGraphMessage>(DispatchGraph);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
@@ -88,37 +87,6 @@ namespace Content.Server.AI.Pathfinding
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -387,19 +387,6 @@ namespace Content.Server.AI.Steering
|
||||
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
|
||||
|
||||
// Move towards it
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
namespace Content.Server.Access
|
||||
{
|
||||
public sealed class AccessReaderChangeMessage : EntityEventArgs
|
||||
public sealed class AccessReaderChangeEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid Sender { get; }
|
||||
|
||||
public bool Enabled { get; }
|
||||
|
||||
public AccessReaderChangeMessage(EntityUid entity, bool enabled)
|
||||
public AccessReaderChangeEvent(EntityUid entity, bool enabled)
|
||||
{
|
||||
Sender = entity;
|
||||
Enabled = enabled;
|
||||
@@ -67,7 +67,7 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
_airtightSystem.SetAirblocked(airtight, collidable);
|
||||
|
||||
// Pathfinding / AI stuff.
|
||||
RaiseLocalEvent(new AccessReaderChangeMessage(uid, collidable));
|
||||
RaiseLocalEvent(new AccessReaderChangeEvent(uid, collidable));
|
||||
|
||||
base.SetCollidable(uid, collidable, door, physics, occluder);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user