2021-10-22 05:31:07 +03:00
using System ;
2020-06-18 22:52:44 +10:00
using System.Collections.Generic ;
using System.Threading ;
2021-06-09 22:19:39 +02:00
using Content.Server.Access ;
2021-10-22 05:31:07 +03:00
using Content.Server.Access.Systems ;
2021-06-09 22:19:39 +02:00
using Content.Server.AI.Pathfinding.Pathfinders ;
using Content.Server.CPUJob.JobQueues ;
using Content.Server.CPUJob.JobQueues.Queues ;
2020-10-14 22:45:53 +02:00
using Content.Shared.GameTicking ;
2020-06-23 02:55:50 +10:00
using Content.Shared.Physics ;
2021-02-11 01:13:03 -08:00
using Robust.Shared.GameObjects ;
2020-06-18 22:52:44 +10:00
using Robust.Shared.IoC ;
using Robust.Shared.Map ;
2020-10-11 15:21:21 +02:00
using Robust.Shared.Maths ;
2021-03-08 04:09:59 +11:00
using Robust.Shared.Physics ;
2020-06-23 02:55:50 +10:00
using Robust.Shared.Utility ;
2020-06-18 22:52:44 +10:00
2021-06-09 22:19:39 +02:00
namespace Content.Server.AI.Pathfinding
2020-06-18 22:52:44 +10:00
{
/ *
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>
2021-06-29 15:56:07 +02:00
public class PathfindingSystem : EntitySystem
2020-06-18 22:52:44 +10:00
{
2020-08-12 01:36:40 +10:00
[Dependency] private readonly IMapManager _mapManager = default ! ;
2020-09-06 16:11:53 +02:00
[Dependency] private readonly IEntityManager _entityManager = default ! ;
2021-10-22 05:31:07 +03:00
[Dependency] private readonly AccessReaderSystem _accessReader = default ! ;
2021-03-08 04:09:59 +11:00
2020-10-11 15:21:21 +02:00
public IReadOnlyDictionary < GridId , Dictionary < Vector2i , PathfindingChunk > > Graph = > _graph ;
2020-11-27 11:00:49 +01:00
private readonly Dictionary < GridId , Dictionary < Vector2i , PathfindingChunk > > _graph = new ( ) ;
2020-08-10 03:33:05 +02:00
2020-11-27 11:00:49 +01:00
private readonly PathfindingJobQueue _pathfindingQueue = new ( ) ;
2020-08-10 03:33:05 +02:00
2020-06-23 02:55:50 +10:00
// Queued pathfinding graph updates
2020-11-27 11:00:49 +01:00
private readonly Queue < CollisionChangeMessage > _collidableUpdateQueue = new ( ) ;
private readonly Queue < MoveEvent > _moveUpdateQueue = new ( ) ;
private readonly Queue < AccessReaderChangeMessage > _accessReaderUpdateQueue = new ( ) ;
private readonly Queue < TileRef > _tileUpdateQueue = new ( ) ;
2020-06-18 22:52:44 +10:00
// Need to store previously known entity positions for collidables for when they move
2021-12-05 18:09:01 +01:00
private readonly Dictionary < EntityUid , PathfindingNode > _lastKnownPositions = new ( ) ;
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
{
2021-12-05 18:09:01 +01:00
if ( ! EntityManager . EntityExists ( update . Owner ) ) continue ;
2021-03-08 04:09:59 +11:00
2020-06-23 02:55:50 +10:00
if ( update . CanCollide )
2020-06-18 22:52:44 +10:00
{
2021-12-05 18:09:01 +01:00
HandleEntityAdd ( update . Owner ) ;
2020-06-18 22:52:44 +10:00
}
2020-06-23 02:55:50 +10:00
else
{
2021-12-05 18:09:01 +01:00
HandleEntityRemove ( update . Owner ) ;
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 ) ;
2020-10-11 15:21:21 +02:00
var Vector2i = new Vector2i ( chunkX , chunkY ) ;
2020-06-18 22:52:44 +10:00
if ( _graph . TryGetValue ( tile . GridIndex , out var chunks ) )
{
2020-10-11 15:21:21 +02:00
if ( ! chunks . ContainsKey ( Vector2i ) )
2020-06-18 22:52:44 +10:00
{
2020-10-11 15:21:21 +02:00
CreateChunk ( tile . GridIndex , Vector2i ) ;
2020-06-18 22:52:44 +10:00
}
2020-10-11 15:21:21 +02:00
return chunks [ Vector2i ] ;
2020-06-18 22:52:44 +10:00
}
2020-10-11 15:21:21 +02:00
var newChunk = CreateChunk ( tile . GridIndex , Vector2i ) ;
2020-06-18 22:52:44 +10:00
return newChunk ;
}
2020-10-11 15:21:21 +02:00
private PathfindingChunk CreateChunk ( GridId gridId , Vector2i indices )
2020-06-18 22:52:44 +10:00
{
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
{
2020-10-11 15:21:21 +02:00
_graph . Add ( gridId , new Dictionary < Vector2i , PathfindingChunk > ( ) ) ;
2020-06-18 22:52:44 +10:00
}
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>
2021-12-05 18:09:01 +01:00
public PathfindingNode GetNode ( EntityUid entity )
2020-07-11 23:09:37 +10:00
{
2021-12-08 13:00:43 +01:00
var tile = _mapManager . GetGrid ( EntityManager . GetComponent < TransformComponent > ( entity ) . GridID ) . GetTileRef ( EntityManager . GetComponent < TransformComponent > ( entity ) . Coordinates ) ;
2020-07-11 23:09:37 +10:00
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 ( )
{
2021-06-29 15:56:07 +02:00
SubscribeLocalEvent < RoundRestartCleanupEvent > ( Reset ) ;
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-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 )
{
2021-08-23 16:00:17 +10:00
if ( ! _mapManager . GridExists ( tile . GridIndex ) ) return ;
2020-06-23 02:55:50 +10:00
var node = GetNode ( tile ) ;
node . UpdateTile ( tile ) ;
}
2020-06-18 22:52:44 +10:00
2021-04-02 20:29:19 +11:00
private void HandleGridRemoval ( MapId mapId , 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
}
2021-03-16 15:50:20 +01:00
private void QueueGridChange ( object? sender , GridChangedEventArgs eventArgs )
2020-06-18 22:52:44 +10:00
{
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
}
}
2021-03-16 15:50:20 +01:00
private void QueueTileChange ( object? sender , TileChangedEventArgs eventArgs )
2020-06-18 22:52:44 +10:00
{
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>
2021-12-05 18:09:01 +01:00
private void HandleEntityAdd ( EntityUid entity )
2020-06-18 22:52:44 +10:00
{
2021-12-08 13:00:43 +01:00
if ( ( ! EntityManager . EntityExists ( entity ) ? EntityLifeStage . Deleted : EntityManager . GetComponent < MetaDataComponent > ( entity ) . EntityLifeStage ) > = EntityLifeStage . Deleted | |
2020-08-12 01:36:40 +10:00
_lastKnownPositions . ContainsKey ( entity ) | |
2021-12-08 13:00:43 +01:00
! EntityManager . TryGetComponent ( entity , out IPhysBody ? physics ) | |
2020-10-11 16:36:58 +02:00
! PathfindingNode . IsRelevant ( entity , physics ) )
2020-06-18 22:52:44 +10:00
{
return ;
}
2020-08-10 03:33:05 +02:00
2021-12-08 13:00:43 +01:00
var grid = _mapManager . GetGrid ( EntityManager . GetComponent < TransformComponent > ( entity ) . GridID ) ;
var tileRef = grid . GetTileRef ( EntityManager . GetComponent < TransformComponent > ( entity ) . Coordinates ) ;
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-10-11 16:36:58 +02:00
node . AddEntity ( entity , physics ) ;
2020-08-12 01:36:40 +10:00
_lastKnownPositions . Add ( entity , node ) ;
2020-06-18 22:52:44 +10:00
}
2021-12-05 18:09:01 +01:00
private void HandleEntityRemove ( EntityUid 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
}
2021-08-21 11:49:31 +02:00
private void QueueMoveEvent ( ref 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.
2021-12-08 13:00:43 +01:00
if ( ( ! EntityManager . EntityExists ( moveEvent . Sender ) ? EntityLifeStage . Deleted : EntityManager . GetComponent < MetaDataComponent > ( moveEvent . Sender ) . EntityLifeStage ) > = EntityLifeStage . Deleted | |
! EntityManager . TryGetComponent ( moveEvent . Sender , out IPhysBody ? physics ) | |
2021-01-11 22:14:01 +11:00
! PathfindingNode . IsRelevant ( moveEvent . Sender , physics ) | |
moveEvent . NewPosition . GetGridId ( EntityManager ) = = GridId . Invalid )
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
2021-12-08 13:00:43 +01:00
var gridBounds = _mapManager . GetGrid ( EntityManager . GetComponent < TransformComponent > ( moveEvent . Sender ) . GridID ) . WorldBounds ;
2020-08-13 14:40:27 +02:00
2021-12-08 13:00:43 +01:00
if ( ! gridBounds . Contains ( EntityManager . GetComponent < TransformComponent > ( moveEvent . Sender ) . 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-11-06 15:04:13 +01:00
var newGridId = moveEvent . NewPosition . GetGridId ( _entityManager ) ;
if ( newGridId = = GridId . Invalid )
{
HandleEntityRemove ( moveEvent . Sender ) ;
return ;
}
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-11-06 15:04:13 +01:00
var newTile = _mapManager . GetGrid ( newGridId ) . 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-10-11 16:36:58 +02:00
newNode . AddEntity ( moveEvent . Sender , physics ) ;
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
2021-12-05 18:09:01 +01:00
public bool CanTraverse ( EntityUid entity , EntityCoordinates coordinates )
2020-06-23 02:55:50 +10:00
{
2020-11-18 15:45:53 +01:00
var gridId = coordinates . GetGridId ( EntityManager ) ;
2020-09-06 16:11:53 +02:00
var tile = _mapManager . GetGrid ( gridId ) . GetTileRef ( coordinates ) ;
2020-06-23 02:55:50 +10:00
var node = GetNode ( tile ) ;
return CanTraverse ( entity , node ) ;
}
2021-12-05 18:09:01 +01:00
public bool CanTraverse ( EntityUid entity , PathfindingNode node )
2020-06-23 02:55:50 +10:00
{
2021-12-08 13:00:43 +01:00
if ( EntityManager . TryGetComponent ( entity , out IPhysBody ? physics ) & &
2020-10-11 16:36:58 +02:00
( physics . CollisionMask & node . BlockedCollisionMask ) ! = 0 )
2020-06-23 02:55:50 +10:00
{
return false ;
}
2021-12-03 15:53:09 +01:00
var access = _accessReader . FindAccessTags ( entity ) ;
2020-06-23 02:55:50 +10:00
foreach ( var reader in node . AccessReaders )
2020-06-18 22:52:44 +10:00
{
2021-10-22 05:31:07 +03:00
if ( ! _accessReader . IsAllowed ( reader , access ) )
2020-06-23 02:55:50 +10:00
{
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
}
2020-10-14 22:45:53 +02:00
2021-06-29 15:56:07 +02:00
public void Reset ( RoundRestartCleanupEvent ev )
2020-10-14 22:45:53 +02:00
{
_graph . Clear ( ) ;
_collidableUpdateQueue . Clear ( ) ;
_moveUpdateQueue . Clear ( ) ;
_accessReaderUpdateQueue . Clear ( ) ;
_tileUpdateQueue . Clear ( ) ;
_lastKnownPositions . Clear ( ) ;
}
2020-06-18 22:52:44 +10:00
}
}