2020-07-19 00:33:02 -07:00
using System ;
2020-06-18 22:52:44 +10:00
using System.Collections.Generic ;
using System.Threading ;
2020-06-23 02:55:50 +10:00
using Content.Server.GameObjects.Components.Access ;
2020-06-18 22:52:44 +10:00
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders ;
using Content.Server.GameObjects.EntitySystems.JobQueues ;
using Content.Server.GameObjects.EntitySystems.JobQueues.Queues ;
2020-06-23 02:55:50 +10:00
using Content.Shared.Physics ;
2020-06-18 22:52:44 +10:00
using Robust.Shared.GameObjects.Components ;
using Robust.Shared.GameObjects.Components.Transform ;
using Robust.Shared.GameObjects.Systems ;
using Robust.Shared.Interfaces.GameObjects ;
using Robust.Shared.Interfaces.Map ;
using Robust.Shared.IoC ;
using Robust.Shared.Map ;
2020-06-23 02:55:50 +10:00
using Robust.Shared.Utility ;
2020-06-18 22:52:44 +10:00
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
{
/ *
2020-08-12 01:36:40 +10:00
// TODO: IMO use rectangular symmetry reduction on the nodes with collision at all. (currently planned to be implemented via AiReachableSystem and expanded later).
2020-06-18 22:52:44 +10:00
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?
2020-08-12 01:36:40 +10:00
TODO : Handle container entities so they ' re not tracked .
2020-06-18 22:52:44 +10:00
* /
/// <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 class PathfindingSystem : EntitySystem
{
2020-08-12 01:36:40 +10:00
[Dependency] private readonly IMapManager _mapManager = default ! ;
2020-06-18 22:52:44 +10:00
public IReadOnlyDictionary < GridId , Dictionary < MapIndices , PathfindingChunk > > Graph = > _graph ;
private readonly Dictionary < GridId , Dictionary < MapIndices , PathfindingChunk > > _graph = new Dictionary < GridId , Dictionary < MapIndices , PathfindingChunk > > ( ) ;
2020-08-10 03:33:05 +02:00
2020-06-18 22:52:44 +10:00
private readonly PathfindingJobQueue _pathfindingQueue = new PathfindingJobQueue ( ) ;
2020-08-10 03:33:05 +02:00
2020-06-23 02:55:50 +10:00
// Queued pathfinding graph updates
2020-07-08 21:05:27 +10:00
private readonly Queue < CollisionChangeMessage > _collidableUpdateQueue = new Queue < CollisionChangeMessage > ( ) ;
2020-06-23 02:55:50 +10:00
private readonly Queue < MoveEvent > _moveUpdateQueue = new Queue < MoveEvent > ( ) ;
private readonly Queue < AccessReaderChangeMessage > _accessReaderUpdateQueue = new Queue < AccessReaderChangeMessage > ( ) ;
private readonly Queue < TileRef > _tileUpdateQueue = new Queue < TileRef > ( ) ;
2020-06-18 22:52:44 +10:00
// Need to store previously known entity positions for collidables for when they move
2020-08-12 01:36:40 +10:00
private readonly Dictionary < IEntity , PathfindingNode > _lastKnownPositions = new Dictionary < IEntity , PathfindingNode > ( ) ;
2020-06-18 22:52:44 +10:00
2020-06-23 02:55:50 +10:00
public const int TrackedCollisionLayers = ( int )
2020-08-10 03:33:05 +02:00
( CollisionGroup . Impassable |
2020-06-23 02:55:50 +10:00
CollisionGroup . MobImpassable |
2020-08-10 03:33:05 +02:00
CollisionGroup . SmallImpassable |
2020-06-23 02:55:50 +10:00
CollisionGroup . VaultImpassable ) ;
2020-08-10 03:33:05 +02:00
2020-06-18 22:52:44 +10:00
/// <summary>
/// Ask for the pathfinder to gimme somethin
/// </summary>
/// <param name="pathfindingArgs"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Job < Queue < TileRef > > RequestPath ( PathfindingArgs pathfindingArgs , CancellationToken cancellationToken )
{
var startNode = GetNode ( pathfindingArgs . Start ) ;
var endNode = GetNode ( pathfindingArgs . End ) ;
var job = new AStarPathfindingJob ( 0.003 , startNode , endNode , pathfindingArgs , cancellationToken ) ;
_pathfindingQueue . EnqueueJob ( job ) ;
return job ;
}
public override void Update ( float frameTime )
{
base . Update ( frameTime ) ;
// Make sure graph is updated, then get pathfinders
ProcessGraphUpdates ( ) ;
_pathfindingQueue . Process ( ) ;
}
private void ProcessGraphUpdates ( )
{
2020-06-23 02:55:50 +10:00
var totalUpdates = 0 ;
2020-08-10 03:33:05 +02:00
2020-06-23 02:55:50 +10:00
foreach ( var update in _collidableUpdateQueue )
2020-06-18 22:52:44 +10:00
{
2020-08-12 01:36:40 +10:00
var entity = EntityManager . GetEntity ( update . Owner ) ;
2020-06-23 02:55:50 +10:00
if ( update . CanCollide )
2020-06-18 22:52:44 +10:00
{
2020-07-08 21:05:27 +10:00
HandleEntityAdd ( entity ) ;
2020-06-18 22:52:44 +10:00
}
2020-06-23 02:55:50 +10:00
else
{
2020-07-08 21:05:27 +10:00
HandleEntityRemove ( entity ) ;
2020-06-23 02:55:50 +10:00
}
totalUpdates + + ;
2020-06-18 22:52:44 +10:00
}
2020-08-10 03:33:05 +02:00
2020-06-23 02:55:50 +10:00
_collidableUpdateQueue . Clear ( ) ;
2020-06-18 22:52:44 +10:00
2020-06-23 02:55:50 +10:00
foreach ( var update in _accessReaderUpdateQueue )
2020-06-18 22:52:44 +10:00
{
2020-06-23 02:55:50 +10:00
if ( update . Enabled )
{
2020-08-12 01:36:40 +10:00
HandleEntityAdd ( update . Sender ) ;
2020-06-23 02:55:50 +10:00
}
else
{
2020-08-12 01:36:40 +10:00
HandleEntityRemove ( update . Sender ) ;
2020-06-23 02:55:50 +10:00
}
totalUpdates + + ;
2020-06-18 22:52:44 +10:00
}
2020-08-10 03:33:05 +02:00
2020-06-23 02:55:50 +10:00
_accessReaderUpdateQueue . Clear ( ) ;
2020-06-18 22:52:44 +10:00
2020-06-23 02:55:50 +10:00
foreach ( var tile in _tileUpdateQueue )
{
HandleTileUpdate ( tile ) ;
totalUpdates + + ;
}
2020-08-10 03:33:05 +02:00
2020-06-23 02:55:50 +10:00
_tileUpdateQueue . Clear ( ) ;
var moveUpdateCount = Math . Max ( 50 - totalUpdates , 0 ) ;
2020-08-10 03:33:05 +02:00
2020-06-23 02:55:50 +10:00
// 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 ;
}
2020-06-18 22:52:44 +10:00
2020-06-23 02:55:50 +10:00
moveUpdateCount = Math . Min ( moveUpdateCount , _moveUpdateQueue . Count ) ;
2020-08-10 03:33:05 +02:00
2020-06-23 02:55:50 +10:00
for ( var i = 0 ; i < moveUpdateCount ; i + + )
{
2020-08-12 01:36:40 +10:00
HandleEntityMove ( _moveUpdateQueue . Dequeue ( ) ) ;
2020-06-23 02:55:50 +10:00
}
2020-08-10 03:33:05 +02:00
2020-06-23 02:55:50 +10:00
DebugTools . Assert ( _moveUpdateQueue . Count < 1000 ) ;
2020-06-18 22:52:44 +10:00
}
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 mapIndices = new MapIndices ( chunkX , chunkY ) ;
if ( _graph . TryGetValue ( tile . GridIndex , out var chunks ) )
{
if ( ! chunks . ContainsKey ( mapIndices ) )
{
CreateChunk ( tile . GridIndex , mapIndices ) ;
}
return chunks [ mapIndices ] ;
}
var newChunk = CreateChunk ( tile . GridIndex , mapIndices ) ;
return newChunk ;
}
private PathfindingChunk CreateChunk ( GridId gridId , MapIndices indices )
{
var newChunk = new PathfindingChunk ( gridId , indices ) ;
2020-06-29 01:43:06 +10:00
if ( ! _graph . ContainsKey ( gridId ) )
2020-06-18 22:52:44 +10:00
{
_graph . Add ( gridId , new Dictionary < MapIndices , PathfindingChunk > ( ) ) ;
}
2020-08-10 03:33:05 +02:00
2020-06-18 22:52:44 +10:00
_graph [ gridId ] . Add ( indices , newChunk ) ;
2020-07-08 21:05:27 +10:00
newChunk . Initialize ( _mapManager . GetGrid ( gridId ) ) ;
2020-08-10 03:33:05 +02:00
2020-06-18 22:52:44 +10:00
return newChunk ;
}
2020-07-11 23:09:37 +10:00
/// <summary>
/// Get the entity's tile position, then get the corresponding node
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public PathfindingNode GetNode ( IEntity entity )
{
var tile = _mapManager . GetGrid ( entity . Transform . GridID ) . GetTileRef ( entity . Transform . GridPosition ) ;
return GetNode ( tile ) ;
}
2020-07-08 21:05:27 +10:00
/// <summary>
/// Return the corresponding PathfindingNode for this tile
/// </summary>
/// <param name="tile"></param>
/// <returns></returns>
2020-06-18 22:52:44 +10:00
public PathfindingNode GetNode ( TileRef tile )
{
var chunk = GetChunk ( tile ) ;
var node = chunk . GetNode ( tile ) ;
return node ;
}
public override void Initialize ( )
{
2020-07-08 21:05:27 +10:00
SubscribeLocalEvent < CollisionChangeMessage > ( QueueCollisionChangeMessage ) ;
SubscribeLocalEvent < MoveEvent > ( QueueMoveEvent ) ;
SubscribeLocalEvent < AccessReaderChangeMessage > ( QueueAccessChangeMessage ) ;
2020-06-18 22:52:44 +10:00
// Handle all the base grid changes
// Anything that affects traversal (i.e. collision layer) is handled separately.
2020-06-23 02:55:50 +10:00
_mapManager . OnGridRemoved + = HandleGridRemoval ;
2020-06-18 22:52:44 +10:00
_mapManager . GridChanged + = QueueGridChange ;
_mapManager . TileChanged + = QueueTileChange ;
}
public override void Shutdown ( )
{
base . Shutdown ( ) ;
2020-07-08 21:05:27 +10:00
UnsubscribeLocalEvent < CollisionChangeMessage > ( ) ;
2020-06-23 02:55:50 +10:00
UnsubscribeLocalEvent < MoveEvent > ( ) ;
UnsubscribeLocalEvent < AccessReaderChangeMessage > ( ) ;
2020-08-10 03:33:05 +02:00
2020-06-23 02:55:50 +10:00
_mapManager . OnGridRemoved - = HandleGridRemoval ;
2020-06-18 22:52:44 +10:00
_mapManager . GridChanged - = QueueGridChange ;
_mapManager . TileChanged - = QueueTileChange ;
}
2020-08-10 03:33:05 +02:00
2020-06-23 02:55:50 +10:00
private void HandleTileUpdate ( TileRef tile )
{
var node = GetNode ( tile ) ;
node . UpdateTile ( tile ) ;
}
2020-06-18 22:52:44 +10:00
2020-06-21 17:28:43 +02:00
public void ResettingCleanup ( )
{
2020-06-23 02:55:50 +10:00
_graph . Clear ( ) ;
_collidableUpdateQueue . Clear ( ) ;
_moveUpdateQueue . Clear ( ) ;
_accessReaderUpdateQueue . Clear ( ) ;
_tileUpdateQueue . Clear ( ) ;
_lastKnownPositions . Clear ( ) ;
2020-06-21 17:28:43 +02:00
}
2020-06-23 02:55:50 +10:00
private void HandleGridRemoval ( GridId gridId )
2020-06-18 22:52:44 +10:00
{
2020-06-23 02:55:50 +10:00
if ( _graph . ContainsKey ( gridId ) )
{
_graph . Remove ( gridId ) ;
}
2020-06-18 22:52:44 +10:00
}
private void QueueGridChange ( object sender , GridChangedEventArgs eventArgs )
{
foreach ( var ( position , _ ) in eventArgs . Modified )
{
2020-06-23 02:55:50 +10:00
_tileUpdateQueue . Enqueue ( eventArgs . Grid . GetTileRef ( position ) ) ;
2020-06-18 22:52:44 +10:00
}
}
private void QueueTileChange ( object sender , TileChangedEventArgs eventArgs )
{
2020-06-23 02:55:50 +10:00
_tileUpdateQueue . Enqueue ( eventArgs . NewTile ) ;
}
2020-07-08 21:05:27 +10:00
private void QueueAccessChangeMessage ( AccessReaderChangeMessage message )
2020-06-23 02:55:50 +10:00
{
_accessReaderUpdateQueue . Enqueue ( message ) ;
}
2020-06-18 22:52:44 +10:00
/// <summary>
2020-07-08 21:05:27 +10:00
/// Tries to add the entity to the relevant pathfinding node
2020-06-18 22:52:44 +10:00
/// </summary>
2020-07-08 21:05:27 +10:00
/// The node will filter it to the correct category (if possible)
2020-06-18 22:52:44 +10:00
/// <param name="entity"></param>
2020-07-08 21:05:27 +10:00
private void HandleEntityAdd ( IEntity entity )
2020-06-18 22:52:44 +10:00
{
2020-08-13 14:40:27 +02:00
if ( entity . Deleted | |
2020-08-12 01:36:40 +10:00
_lastKnownPositions . ContainsKey ( entity ) | |
2020-08-13 14:40:27 +02:00
! entity . TryGetComponent ( out ICollidableComponent collidableComponent ) | |
2020-08-12 01:36:40 +10:00
! PathfindingNode . IsRelevant ( entity , collidableComponent ) )
2020-06-18 22:52:44 +10:00
{
return ;
}
2020-08-10 03:33:05 +02:00
2020-06-18 22:52:44 +10:00
var grid = _mapManager . GetGrid ( entity . Transform . GridID ) ;
var tileRef = grid . GetTileRef ( entity . Transform . GridPosition ) ;
2020-07-08 21:05:27 +10:00
2020-06-18 22:52:44 +10:00
var chunk = GetChunk ( tileRef ) ;
var node = chunk . GetNode ( tileRef ) ;
2020-08-12 01:36:40 +10:00
node . AddEntity ( entity , collidableComponent ) ;
_lastKnownPositions . Add ( entity , node ) ;
2020-06-18 22:52:44 +10:00
}
2020-07-08 21:05:27 +10:00
private void HandleEntityRemove ( IEntity entity )
2020-06-18 22:52:44 +10:00
{
2020-08-12 01:36:40 +10:00
if ( ! _lastKnownPositions . TryGetValue ( entity , out var node ) )
2020-06-18 22:52:44 +10:00
{
return ;
}
2020-06-23 02:55:50 +10:00
node . RemoveEntity ( entity ) ;
2020-08-12 01:36:40 +10:00
_lastKnownPositions . Remove ( entity ) ;
2020-06-18 22:52:44 +10:00
}
2020-07-08 21:05:27 +10:00
private void QueueMoveEvent ( MoveEvent moveEvent )
2020-06-18 22:52:44 +10:00
{
2020-06-23 02:55:50 +10:00
_moveUpdateQueue . Enqueue ( moveEvent ) ;
2020-06-18 22:52:44 +10:00
}
2020-07-08 21:05:27 +10:00
/// <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>
2020-08-12 01:36:40 +10:00
private void HandleEntityMove ( MoveEvent moveEvent )
2020-06-18 22:52:44 +10:00
{
2020-08-12 01:36:40 +10:00
// If we've moved to space or the likes then remove us.
2020-08-13 14:40:27 +02:00
if ( moveEvent . Sender . Deleted | |
2020-08-12 01:36:40 +10:00
! moveEvent . Sender . TryGetComponent ( out ICollidableComponent collidableComponent ) | |
! PathfindingNode . IsRelevant ( moveEvent . Sender , collidableComponent ) )
2020-06-18 22:52:44 +10:00
{
2020-08-12 01:36:40 +10:00
HandleEntityRemove ( moveEvent . Sender ) ;
2020-06-18 22:52:44 +10:00
return ;
}
2020-08-13 14:40:27 +02:00
2020-08-12 01:36:40 +10:00
// Memory leak protection until grid parenting confirmed fix / you REALLY need the performance
var gridBounds = _mapManager . GetGrid ( moveEvent . Sender . Transform . GridID ) . WorldBounds ;
2020-08-13 14:40:27 +02:00
2020-08-12 01:36:40 +10:00
if ( ! gridBounds . Contains ( moveEvent . Sender . Transform . WorldPosition ) )
2020-06-18 22:52:44 +10:00
{
2020-07-08 21:05:27 +10:00
HandleEntityRemove ( moveEvent . Sender ) ;
2020-06-18 22:52:44 +10:00
return ;
}
2020-08-13 14:40:27 +02:00
2020-08-12 01:36:40 +10:00
// 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 ;
}
2020-08-10 03:33:05 +02:00
2020-08-12 01:36:40 +10:00
// 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.
2020-06-18 22:52:44 +10:00
var newTile = _mapManager . GetGrid ( moveEvent . NewPosition . GridID ) . GetTileRef ( moveEvent . NewPosition ) ;
2020-08-13 14:40:27 +02:00
2020-07-08 21:05:27 +10:00
if ( oldNode = = null | | oldNode . TileRef = = newTile )
2020-06-18 22:52:44 +10:00
{
return ;
}
2020-07-08 21:05:27 +10:00
var newNode = GetNode ( newTile ) ;
2020-08-12 01:36:40 +10:00
_lastKnownPositions [ moveEvent . Sender ] = newNode ;
2020-06-18 22:52:44 +10:00
2020-07-08 21:05:27 +10:00
oldNode . RemoveEntity ( moveEvent . Sender ) ;
2020-08-12 01:36:40 +10:00
newNode . AddEntity ( moveEvent . Sender , collidableComponent ) ;
2020-06-18 22:52:44 +10:00
}
2020-07-08 21:05:27 +10:00
private void QueueCollisionChangeMessage ( CollisionChangeMessage collisionMessage )
2020-06-18 22:52:44 +10:00
{
2020-07-08 21:05:27 +10:00
_collidableUpdateQueue . Enqueue ( collisionMessage ) ;
2020-06-23 02:55:50 +10:00
}
// 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 ( IEntity entity , GridCoordinates grid )
{
var tile = _mapManager . GetGrid ( grid . GridID ) . GetTileRef ( grid ) ;
var node = GetNode ( tile ) ;
return CanTraverse ( entity , node ) ;
}
public bool CanTraverse ( IEntity entity , PathfindingNode node )
{
2020-07-19 00:33:02 -07:00
if ( entity . TryGetComponent ( out ICollidableComponent collidableComponent ) & &
2020-06-23 02:55:50 +10:00
( collidableComponent . CollisionMask & node . BlockedCollisionMask ) ! = 0 )
{
return false ;
}
var access = AccessReader . FindAccessTags ( entity ) ;
2020-08-10 03:33:05 +02:00
2020-06-23 02:55:50 +10:00
foreach ( var reader in node . AccessReaders )
2020-06-18 22:52:44 +10:00
{
2020-06-23 02:55:50 +10:00
if ( ! reader . IsAllowed ( access ) )
{
return false ;
}
2020-06-18 22:52:44 +10:00
}
2020-06-23 02:55:50 +10:00
return true ;
2020-06-18 22:52:44 +10:00
}
}
}