2021-06-09 22:19:39 +02:00
using Content.Server.AI.Pathfinding.Pathfinders ;
2021-12-26 17:07:28 +13:00
using Content.Shared.Access.Systems ;
2020-07-11 23:09:37 +10:00
using Content.Shared.AI ;
2020-10-14 22:45:53 +02:00
using Content.Shared.GameTicking ;
2020-07-11 23:09:37 +10:00
using JetBrains.Annotations ;
2021-03-08 04:09:59 +11:00
using Robust.Server.Player ;
2020-07-11 23:09:37 +10:00
using Robust.Shared.Map ;
2021-03-08 04:09:59 +11:00
using Robust.Shared.Physics ;
2021-02-11 01:13:03 -08:00
using Robust.Shared.Timing ;
2020-07-11 23:09:37 +10:00
using Robust.Shared.Utility ;
2021-06-09 22:19:39 +02:00
namespace Content.Server.AI.Pathfinding.Accessible
2020-07-11 23:09:37 +10:00
{
/// <summary>
/// Determines whether an AI has access to a specific pathfinding node.
/// </summary>
/// Long-term can be used to do hierarchical pathfinding
[UsedImplicitly]
2021-06-29 15:56:07 +02:00
public sealed class AiReachableSystem : EntitySystem
2020-07-11 23:09:37 +10:00
{
/ *
* The purpose of this is to provide a higher - level / hierarchical abstraction of the actual pathfinding graph
* The goal is so that we can more quickly discern if a specific node is reachable or not rather than
* Pathfinding the entire graph .
*
* There ' s a lot of different implementations of hierarchical or some variation of it : HPA * , PRA , HAA * , etc .
* ( HPA * technically caches the edge nodes of each chunk ) , e . g . Rimworld , Factorio , etc .
* so we ' ll just write one with SS14 ' s requirements in mind .
*
* There ' s probably a better data structure to use though you ' d need to benchmark multiple ones to compare ,
* at the very least on the memory side it could definitely be better .
* /
2020-08-24 14:10:28 +02:00
[Dependency] private readonly IMapManager _mapManager = default ! ;
[Dependency] private readonly IGameTiming _gameTiming = default ! ;
2021-07-26 12:58:17 +02:00
[Dependency] private readonly PathfindingSystem _pathfindingSystem = default ! ;
2021-10-22 05:31:07 +03:00
[Dependency] private readonly AccessReaderSystem _accessReader = default ! ;
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
/// <summary>
/// Queued region updates
/// </summary>
2020-11-27 11:00:49 +01:00
private readonly HashSet < PathfindingChunk > _queuedUpdates = new ( ) ;
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
// Oh god the nesting. Shouldn't need to go beyond this
/// <summary>
/// The corresponding regions for each PathfindingChunk.
/// Regions are groups of nodes with the same profile (for pathfinding purposes)
/// i.e. same collision, not-space, same access, etc.
/// </summary>
2022-06-11 18:54:41 -07:00
private readonly Dictionary < EntityUid , Dictionary < PathfindingChunk , HashSet < PathfindingRegion > > > _regions =
2020-11-27 11:00:49 +01:00
new ( ) ;
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
/// <summary>
/// Minimum time for the cached reachable regions to be stored
/// </summary>
private const float MinCacheTime = 1.0f ;
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
// Cache what regions are accessible from this region. Cached per ReachableArgs
// so multiple entities in the same region with the same args should all be able to share their reachable lookup
// Also need to store when we cached it to know if it's stale if the chunks have updated
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
// TODO: There's probably a more memory-efficient way to cache this
// Then again, there's likely also a more memory-efficient way to implement regions.
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
// Also, didn't use a dictionary because there didn't seem to be a clean way to do the lookup
// Plus this way we can check if everything is equal except for vision so an entity with a lower vision radius can use an entity with a higher vision radius' cached result
2020-11-21 14:02:00 +01:00
private readonly Dictionary < ReachableArgs , Dictionary < PathfindingRegion , ( TimeSpan CacheTime , HashSet < PathfindingRegion > Regions ) > > _cachedAccessible =
2020-11-27 11:00:49 +01:00
new ( ) ;
2020-08-13 14:40:27 +02:00
2020-11-27 11:00:49 +01:00
private readonly List < PathfindingRegion > _queuedCacheDeletions = new ( ) ;
2020-07-11 23:09:37 +10:00
#if DEBUG
2021-03-08 04:09:59 +11:00
private HashSet < IPlayerSession > _subscribedSessions = new ( ) ;
2020-07-11 23:09:37 +10:00
private int _runningCacheIdx = 0 ;
#endif
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
public override void Initialize ( )
{
2021-06-29 15:56:07 +02:00
SubscribeLocalEvent < RoundRestartCleanupEvent > ( Reset ) ;
2020-07-11 23:09:37 +10:00
SubscribeLocalEvent < PathfindingChunkUpdateMessage > ( RecalculateNodeRegions ) ;
2022-03-09 07:39:03 +01:00
SubscribeLocalEvent < GridRemovalEvent > ( GridRemoved ) ;
2020-07-11 23:09:37 +10:00
#if DEBUG
2021-03-08 04:09:59 +11:00
SubscribeNetworkEvent < SharedAiDebug . SubscribeReachableMessage > ( HandleSubscription ) ;
SubscribeNetworkEvent < SharedAiDebug . UnsubscribeReachableMessage > ( HandleUnsubscription ) ;
2020-07-11 23:09:37 +10:00
#endif
2020-07-17 11:17:42 +02:00
}
2020-08-13 14:40:27 +02:00
2021-04-09 16:08:12 +02:00
public override void Shutdown ( )
{
base . Shutdown ( ) ;
_queuedUpdates . Clear ( ) ;
_regions . Clear ( ) ;
_cachedAccessible . Clear ( ) ;
_queuedCacheDeletions . Clear ( ) ;
}
2022-03-09 07:39:03 +01:00
private void GridRemoved ( GridRemovalEvent ev )
2020-07-17 11:17:42 +02:00
{
2022-06-29 00:56:02 +12:00
_regions . Remove ( ev . EntityUid ) ;
2020-07-11 23:09:37 +10:00
}
public override void Update ( float frameTime )
{
base . Update ( frameTime ) ;
foreach ( var chunk in _queuedUpdates )
{
GenerateRegions ( chunk ) ;
}
2021-03-08 04:09:59 +11:00
// TODO: Only send diffs instead
2020-07-11 23:09:37 +10:00
#if DEBUG
2021-03-08 04:09:59 +11:00
if ( _subscribedSessions . Count > 0 & & _queuedUpdates . Count > 0 )
2020-07-11 23:09:37 +10:00
{
foreach ( var ( gridId , regs ) in _regions )
{
if ( regs . Count > 0 )
{
SendRegionsDebugMessage ( gridId ) ;
}
}
}
#endif
_queuedUpdates . Clear ( ) ;
2020-07-28 00:11:07 +10:00
foreach ( var region in _queuedCacheDeletions )
{
ClearCache ( region ) ;
}
2020-08-06 00:19:00 +10:00
2020-07-28 00:11:07 +10:00
_queuedCacheDeletions . Clear ( ) ;
2020-07-11 23:09:37 +10:00
}
2021-03-08 04:09:59 +11:00
#if DEBUG
private void HandleSubscription ( SharedAiDebug . SubscribeReachableMessage message , EntitySessionEventArgs eventArgs )
{
_subscribedSessions . Add ( ( IPlayerSession ) eventArgs . SenderSession ) ;
foreach ( var ( gridId , _ ) in _regions )
{
SendRegionsDebugMessage ( gridId ) ;
}
}
private void HandleUnsubscription ( SharedAiDebug . UnsubscribeReachableMessage message , EntitySessionEventArgs eventArgs )
{
_subscribedSessions . Remove ( ( IPlayerSession ) eventArgs . SenderSession ) ;
}
#endif
2020-07-11 23:09:37 +10:00
private void RecalculateNodeRegions ( PathfindingChunkUpdateMessage message )
{
// TODO: Only need to do changed nodes ideally
// For now this is fine but it's a low-hanging fruit optimisation
_queuedUpdates . Add ( message . Chunk ) ;
}
/// <summary>
/// Can the entity reach the target?
/// </summary>
/// First it does a quick check to see if there are any traversable nodes in range.
/// Then it will go through the regions to try and see if there's a region connection between the target and itself
/// Will used a cached region if available
/// <param name="entity"></param>
/// <param name="target"></param>
/// <param name="range"></param>
/// <returns></returns>
2021-12-05 18:09:01 +01:00
public bool CanAccess ( EntityUid entity , EntityUid target , float range = 0.0f )
2020-07-11 23:09:37 +10:00
{
2022-05-03 18:25:01 +10:00
var xform = EntityManager . GetComponent < TransformComponent > ( target ) ;
2021-10-10 10:56:47 +02:00
// TODO: Handle this gracefully instead of just failing.
2022-06-20 12:14:35 +12:00
if ( xform . GridUid = = null )
2021-10-10 10:56:47 +02:00
return false ;
2022-06-20 12:14:35 +12:00
var targetTile = _mapManager . GetGrid ( xform . GridUid . Value ) . GetTileRef ( xform . Coordinates ) ;
2020-07-11 23:09:37 +10:00
var targetNode = _pathfindingSystem . GetNode ( targetTile ) ;
var collisionMask = 0 ;
2021-12-07 23:48:34 +11:00
if ( EntityManager . TryGetComponent ( entity , out IPhysBody ? physics ) )
2020-07-11 23:09:37 +10:00
{
2020-10-11 16:36:58 +02:00
collisionMask = physics . CollisionMask ;
2020-07-11 23:09:37 +10:00
}
2021-12-03 15:53:09 +01:00
var access = _accessReader . FindAccessTags ( entity ) ;
2020-07-11 23:09:37 +10:00
// We'll do a quick traversable check before going through regions
// If we can't access it we'll try to get a valid node in range (this is essentially an early-out)
if ( ! PathfindingHelpers . Traversable ( collisionMask , access , targetNode ) )
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
if ( range = = 0.0f )
{
return false ;
}
2021-12-03 15:53:09 +01:00
var pathfindingArgs = new PathfindingArgs ( entity , access , collisionMask , default , targetTile , range ) ;
2020-07-11 23:09:37 +10:00
foreach ( var node in BFSPathfinder . GetNodesInRange ( pathfindingArgs , false ) )
{
targetNode = node ;
}
}
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
return CanAccess ( entity , targetNode ) ;
}
2021-12-05 18:09:01 +01:00
public bool CanAccess ( EntityUid entity , PathfindingNode targetNode )
2020-07-11 23:09:37 +10:00
{
2022-05-03 18:25:01 +10:00
var xform = EntityManager . GetComponent < TransformComponent > ( entity ) ;
2022-06-20 12:14:35 +12:00
if ( xform . GridUid ! = targetNode . TileRef . GridUid | | xform . GridUid = = null )
2020-07-11 23:09:37 +10:00
return false ;
2020-07-17 11:17:42 +02:00
2022-06-20 12:14:35 +12:00
var entityTile = _mapManager . GetGrid ( xform . GridUid . Value ) . GetTileRef ( xform . Coordinates ) ;
2020-07-11 23:09:37 +10:00
var entityNode = _pathfindingSystem . GetNode ( entityTile ) ;
var entityRegion = GetRegion ( entityNode ) ;
var targetRegion = GetRegion ( targetNode ) ;
// TODO: Regional pathfind from target to entity
// Early out
if ( entityRegion = = targetRegion )
{
return true ;
}
// We'll go from target's position to us because most of the time it's probably in a locked room rather than vice versa
var reachableArgs = ReachableArgs . GetArgs ( entity ) ;
var reachableRegions = GetReachableRegions ( reachableArgs , targetRegion ) ;
2021-03-16 15:50:20 +01:00
return entityRegion ! = null & & reachableRegions . Contains ( entityRegion ) ;
2020-07-11 23:09:37 +10:00
}
/// <summary>
/// Retrieve the reachable regions
/// </summary>
/// <param name="reachableArgs"></param>
/// <param name="region"></param>
/// <returns></returns>
2021-03-16 15:50:20 +01:00
public HashSet < PathfindingRegion > GetReachableRegions ( ReachableArgs reachableArgs , PathfindingRegion ? region )
2020-07-11 23:09:37 +10:00
{
// if we're on a node that's not tracked at all atm then region will be null
2020-08-06 00:19:00 +10:00
if ( region = = null )
2020-07-11 23:09:37 +10:00
{
return new HashSet < PathfindingRegion > ( ) ;
}
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
var cachedArgs = GetCachedArgs ( reachableArgs ) ;
( TimeSpan CacheTime , HashSet < PathfindingRegion > Regions ) cached ;
if ( ! IsCacheValid ( cachedArgs , region ) )
{
cached = GetVisionReachable ( cachedArgs , region ) ;
_cachedAccessible [ cachedArgs ] [ region ] = cached ;
#if DEBUG
SendRegionCacheMessage ( region . ParentChunk . GridId , cached . Regions , false ) ;
#endif
}
else
{
cached = _cachedAccessible [ cachedArgs ] [ region ] ;
#if DEBUG
SendRegionCacheMessage ( region . ParentChunk . GridId , cached . Regions , true ) ;
#endif
}
return cached . Regions ;
}
/// <summary>
/// Get any adequate cached args if possible, otherwise just use ours
/// </summary>
/// Essentially any args that have the same access AND >= our vision radius can be used
/// <param name="accessibleArgs"></param>
/// <returns></returns>
private ReachableArgs GetCachedArgs ( ReachableArgs accessibleArgs )
{
2021-03-16 15:50:20 +01:00
ReachableArgs ? foundArgs = null ;
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
foreach ( var ( cachedAccessible , _ ) in _cachedAccessible )
{
if ( Equals ( cachedAccessible . Access , accessibleArgs . Access ) & &
cachedAccessible . CollisionMask = = accessibleArgs . CollisionMask & &
cachedAccessible . VisionRadius < = accessibleArgs . VisionRadius )
{
foundArgs = cachedAccessible ;
break ;
}
}
return foundArgs ? ? accessibleArgs ;
}
/// <summary>
/// Checks whether there's a valid cache for our accessibility args.
/// Most regular mobs can share their cached accessibility with each other
/// </summary>
/// Will also remove it from the cache if it is invalid
/// <param name="accessibleArgs"></param>
/// <param name="region"></param>
/// <returns></returns>
private bool IsCacheValid ( ReachableArgs accessibleArgs , PathfindingRegion region )
{
if ( ! _cachedAccessible . TryGetValue ( accessibleArgs , out var cachedArgs ) )
{
_cachedAccessible . Add ( accessibleArgs , new Dictionary < PathfindingRegion , ( TimeSpan , HashSet < PathfindingRegion > ) > ( ) ) ;
return false ;
}
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
if ( ! cachedArgs . TryGetValue ( region , out var regionCache ) )
{
return false ;
}
// Just so we don't invalidate the cache every tick we'll store it for a minimum amount of time
var currentTime = _gameTiming . CurTime ;
if ( ( currentTime - regionCache . CacheTime ) . TotalSeconds < MinCacheTime )
{
return true ;
}
var checkedAccess = new HashSet < PathfindingRegion > ( ) ;
// Check if cache is stale
foreach ( var accessibleRegion in regionCache . Regions )
{
if ( checkedAccess . Contains ( accessibleRegion ) ) continue ;
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
// Any applicable chunk has been invalidated OR one of our neighbors has been invalidated (i.e. new connections)
// TODO: Could look at storing the TimeSpan directly on the region so our neighbor can tell us straight-up
if ( accessibleRegion . ParentChunk . LastUpdate > regionCache . CacheTime )
{
// Remove the stale cache, to be updated later
_cachedAccessible [ accessibleArgs ] . Remove ( region ) ;
return false ;
}
foreach ( var neighbor in accessibleRegion . Neighbors )
{
if ( checkedAccess . Contains ( neighbor ) ) continue ;
if ( neighbor . ParentChunk . LastUpdate > regionCache . CacheTime )
{
_cachedAccessible [ accessibleArgs ] . Remove ( region ) ;
return false ;
}
checkedAccess . Add ( neighbor ) ;
}
checkedAccess . Add ( accessibleRegion ) ;
}
return true ;
}
/// <summary>
/// Caches the entity's nearby accessible regions in vision radius
/// </summary>
/// Longer-term TODO: Hierarchical pathfinding in which case this function would probably get bulldozed, BRRRTT
/// <param name="reachableArgs"></param>
/// <param name="entityRegion"></param>
private ( TimeSpan , HashSet < PathfindingRegion > ) GetVisionReachable ( ReachableArgs reachableArgs , PathfindingRegion entityRegion )
{
var openSet = new Queue < PathfindingRegion > ( ) ;
openSet . Enqueue ( entityRegion ) ;
var closedSet = new HashSet < PathfindingRegion > ( ) ;
var accessible = new HashSet < PathfindingRegion > { entityRegion } ;
while ( openSet . Count > 0 )
{
var region = openSet . Dequeue ( ) ;
closedSet . Add ( region ) ;
foreach ( var neighbor in region . Neighbors )
{
2020-08-06 00:19:00 +10:00
if ( closedSet . Contains ( neighbor ) )
2020-07-11 23:09:37 +10:00
{
continue ;
}
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
// Distance is an approximation here so we'll be generous with it
// TODO: Could do better; the fewer nodes the better it is.
if ( ! neighbor . RegionTraversable ( reachableArgs ) | |
neighbor . Distance ( entityRegion ) > reachableArgs . VisionRadius + 1 )
{
closedSet . Add ( neighbor ) ;
continue ;
}
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
openSet . Enqueue ( neighbor ) ;
accessible . Add ( neighbor ) ;
}
}
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
return ( _gameTiming . CurTime , accessible ) ;
}
/// <summary>
/// Grab the related cardinal nodes and if they're in different regions then add to our edge and their edge
/// </summary>
/// Implicitly they would've already been merged if possible
/// <param name="region"></param>
/// <param name="node"></param>
private void UpdateRegionEdge ( PathfindingRegion region , PathfindingNode node )
{
DebugTools . Assert ( region . Nodes . Contains ( node ) ) ;
// Originally I tried just doing bottom and left but that doesn't work as the chunk update order is not guaranteed
var checkDirections = new [ ] { Direction . East , Direction . South , Direction . West , Direction . North } ;
foreach ( var direction in checkDirections )
{
var directionNode = node . GetNeighbor ( direction ) ;
if ( directionNode = = null ) continue ;
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
var directionRegion = GetRegion ( directionNode ) ;
if ( directionRegion = = null | | directionRegion = = region ) continue ;
region . Neighbors . Add ( directionRegion ) ;
directionRegion . Neighbors . Add ( region ) ;
}
}
/// <summary>
/// Get the current region for this entity
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
2021-12-05 18:09:01 +01:00
public PathfindingRegion ? GetRegion ( EntityUid entity )
2020-07-11 23:09:37 +10:00
{
2022-05-03 18:25:01 +10:00
var xform = EntityManager . GetComponent < TransformComponent > ( entity ) ;
2022-06-20 12:14:35 +12:00
if ( xform . GridUid = = null )
2021-03-31 12:41:23 -07:00
{
return null ;
}
2022-06-20 12:14:35 +12:00
var entityTile = _mapManager . GetGrid ( xform . GridUid . Value ) . GetTileRef ( xform . Coordinates ) ;
2020-07-11 23:09:37 +10:00
var entityNode = _pathfindingSystem . GetNode ( entityTile ) ;
return GetRegion ( entityNode ) ;
}
/// <summary>
/// Get the current region for this node
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
2021-03-16 15:50:20 +01:00
public PathfindingRegion ? GetRegion ( PathfindingNode node )
2020-07-11 23:09:37 +10:00
{
// Not sure on the best way to optimise this
// On the one hand, just storing each node's region is faster buuutttt muh memory
// On the other hand, you might need O(n) lookups on regions for each chunk, though it's probably not too bad with smaller chunk sizes?
// Someone smarter than me will know better
var parentChunk = node . ParentChunk ;
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
// No guarantee the node even has a region yet (if we're doing neighbor lookups)
if ( ! _regions [ parentChunk . GridId ] . TryGetValue ( parentChunk , out var regions ) )
{
return null ;
}
foreach ( var region in regions )
{
if ( region . Nodes . Contains ( node ) )
{
return region ;
}
}
// Longer term this will probably be guaranteed a region but for now space etc. are no region
return null ;
}
/// <summary>
/// Add this node to the relevant region.
/// </summary>
/// <param name="node"></param>
2020-08-01 22:13:44 +10:00
/// <param name="existingRegions">The cached region for each node</param>
/// <param name="chunkRegions">The existing regions in the chunk</param>
2020-07-11 23:09:37 +10:00
/// <param name="x">This is already calculated in advance so may as well re-use it</param>
/// <param name="y">This is already calculated in advance so may as well re-use it</param>
/// <returns></returns>
2021-03-16 15:50:20 +01:00
private PathfindingRegion ? CalculateNode (
2020-08-13 14:40:27 +02:00
PathfindingNode node ,
2020-08-01 22:13:44 +10:00
Dictionary < PathfindingNode , PathfindingRegion > existingRegions ,
HashSet < PathfindingRegion > chunkRegions ,
int x , int y )
2020-07-11 23:09:37 +10:00
{
DebugTools . Assert ( _regions . ContainsKey ( node . ParentChunk . GridId ) ) ;
DebugTools . Assert ( _regions [ node . ParentChunk . GridId ] . ContainsKey ( node . ParentChunk ) ) ;
// TODO For now we don't have these regions but longer-term yeah sure
if ( node . BlockedCollisionMask ! = 0x0 | | node . TileRef . Tile . IsEmpty )
{
return null ;
}
var parentChunk = node . ParentChunk ;
// Doors will be their own separate region
// We won't store them in existingRegions so they don't show up and can't be connected to (at least for now)
if ( node . AccessReaders . Count > 0 )
{
var region = new PathfindingRegion ( node , new HashSet < PathfindingNode > ( 1 ) { node } , true ) ;
_regions [ parentChunk . GridId ] [ parentChunk ] . Add ( region ) ;
UpdateRegionEdge ( region , node ) ;
return region ;
}
// Relative x and y of the chunk
// If one of our bottom / left neighbors are in a region try to join them
// Otherwise, make our own region.
var leftNeighbor = x > 0 ? parentChunk . Nodes [ x - 1 , y ] : null ;
var bottomNeighbor = y > 0 ? parentChunk . Nodes [ x , y - 1 ] : null ;
2021-03-16 15:50:20 +01:00
PathfindingRegion ? leftRegion ;
PathfindingRegion ? bottomRegion ;
2020-07-11 23:09:37 +10:00
// We'll check if our left or down neighbors are already in a region and join them
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
// Is left node valid to connect to
if ( leftNeighbor ! = null & &
2020-07-17 11:17:42 +02:00
existingRegions . TryGetValue ( leftNeighbor , out leftRegion ) & &
2020-07-11 23:09:37 +10:00
! leftRegion . IsDoor )
{
// We'll try and connect the left node's region to the bottom region if they're separate (yay merge)
2020-08-13 14:40:27 +02:00
if ( bottomNeighbor ! = null & &
2020-08-01 22:13:44 +10:00
existingRegions . TryGetValue ( bottomNeighbor , out bottomRegion ) & &
2020-08-13 14:40:27 +02:00
bottomRegion ! = leftRegion & &
2020-07-11 23:09:37 +10:00
! bottomRegion . IsDoor )
{
bottomRegion . Add ( node ) ;
existingRegions . Add ( node , bottomRegion ) ;
2020-08-06 00:19:00 +10:00
MergeInto ( leftRegion , bottomRegion , existingRegions ) ;
2020-08-13 14:40:27 +02:00
2020-08-01 22:13:44 +10:00
// Cleanup leftRegion
// MergeInto will remove it from the overall region chunk cache while we need to remove it from
// our short-term ones (chunkRegions and existingRegions)
chunkRegions . Remove ( leftRegion ) ;
foreach ( var leftNode in leftRegion . Nodes )
{
existingRegions [ leftNode ] = bottomRegion ;
}
2020-08-13 14:40:27 +02:00
2020-07-11 23:09:37 +10:00
return bottomRegion ;
}
leftRegion . Add ( node ) ;
existingRegions . Add ( node , leftRegion ) ;
UpdateRegionEdge ( leftRegion , node ) ;
return leftRegion ;
}
//Is bottom node valid to connect to
2020-07-17 11:17:42 +02:00
if ( bottomNeighbor ! = null & &
existingRegions . TryGetValue ( bottomNeighbor , out bottomRegion ) & &
2020-07-11 23:09:37 +10:00
! bottomRegion . IsDoor )
{
bottomRegion . Add ( node ) ;
existingRegions . Add ( node , bottomRegion ) ;
UpdateRegionEdge ( bottomRegion , node ) ;
return bottomRegion ;
}
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
// If we can't join an existing region then we'll make our own
var newRegion = new PathfindingRegion ( node , new HashSet < PathfindingNode > { node } , node . AccessReaders . Count > 0 ) ;
_regions [ parentChunk . GridId ] [ parentChunk ] . Add ( newRegion ) ;
existingRegions . Add ( node , newRegion ) ;
UpdateRegionEdge ( newRegion , node ) ;
return newRegion ;
}
/// <summary>
/// Combines the two regions into one bigger region
/// </summary>
/// <param name="source"></param>
/// <param name="target"></param>
2021-03-16 15:50:20 +01:00
private void MergeInto ( PathfindingRegion source , PathfindingRegion target , Dictionary < PathfindingNode , PathfindingRegion > ? existingRegions = null )
2020-07-11 23:09:37 +10:00
{
DebugTools . AssertNotNull ( source ) ;
DebugTools . AssertNotNull ( target ) ;
2020-08-01 22:13:44 +10:00
DebugTools . Assert ( source ! = target ) ;
2020-07-11 23:09:37 +10:00
foreach ( var node in source . Nodes )
{
target . Add ( node ) ;
}
2020-08-06 00:19:00 +10:00
if ( existingRegions ! = null )
{
foreach ( var node in source . Nodes )
{
existingRegions [ node ] = target ;
}
}
2020-07-11 23:09:37 +10:00
source . Shutdown ( ) ;
2020-07-28 00:11:07 +10:00
// This doesn't check the cachedaccessible to see if it's reachable but maybe it should?
// Although currently merge gets spammed so maybe when some other stuff is improved
// MergeInto is also only called by GenerateRegions currently so nothing should hold onto the original region
2020-07-11 23:09:37 +10:00
_regions [ source . ParentChunk . GridId ] [ source . ParentChunk ] . Remove ( source ) ;
foreach ( var node in target . Nodes )
{
UpdateRegionEdge ( target , node ) ;
}
}
2020-07-17 11:17:42 +02:00
2020-07-28 00:11:07 +10:00
/// <summary>
/// Remove the cached accessibility lookup for this region
/// </summary>
/// <param name="region"></param>
private void ClearCache ( PathfindingRegion region )
{
2020-08-06 00:19:00 +10:00
DebugTools . Assert ( region . Deleted ) ;
2020-08-13 14:40:27 +02:00
2020-07-28 00:11:07 +10:00
// Need to forcibly clear cache for ourself and anything that includes us
foreach ( var ( _ , cachedRegions ) in _cachedAccessible )
{
if ( cachedRegions . ContainsKey ( region ) )
{
cachedRegions . Remove ( region ) ;
}
// Seemed like the safest way to remove this
// We could just have GetVisionAccessible remove us if it can tell we're deleted but that
// seems like it could be unreliable
var regionsToClear = new List < PathfindingRegion > ( ) ;
2020-08-13 14:40:27 +02:00
2020-07-28 00:11:07 +10:00
foreach ( var ( otherRegion , cache ) in cachedRegions )
{
if ( cache . Regions . Contains ( region ) )
{
regionsToClear . Add ( otherRegion ) ;
}
}
foreach ( var otherRegion in regionsToClear )
{
cachedRegions . Remove ( otherRegion ) ;
}
}
2020-08-13 14:40:27 +02:00
2020-08-06 00:19:00 +10:00
#if DEBUG
2020-08-13 14:40:27 +02:00
if ( _regions . TryGetValue ( region . ParentChunk . GridId , out var chunks ) & &
2020-08-06 00:19:00 +10:00
chunks . TryGetValue ( region . ParentChunk , out var regions ) )
{
DebugTools . Assert ( ! regions . Contains ( region ) ) ;
}
#endif
2020-07-28 00:11:07 +10:00
}
2020-07-11 23:09:37 +10:00
/// <summary>
/// Generate all of the regions within a chunk
/// </summary>
/// These can't across over into another chunk and doors are their own region
/// <param name="chunk"></param>
private void GenerateRegions ( PathfindingChunk chunk )
{
2020-10-21 17:13:41 +02:00
// Grid deleted while update queued, or invalid grid.
2020-08-26 00:44:23 +10:00
if ( ! _mapManager . TryGetGrid ( chunk . GridId , out _ ) )
{
return ;
}
2020-09-06 16:11:53 +02:00
2020-07-11 23:09:37 +10:00
if ( ! _regions . ContainsKey ( chunk . GridId ) )
{
_regions . Add ( chunk . GridId , new Dictionary < PathfindingChunk , HashSet < PathfindingRegion > > ( ) ) ;
}
if ( _regions [ chunk . GridId ] . TryGetValue ( chunk , out var regions ) )
{
foreach ( var region in regions )
{
2020-07-28 00:11:07 +10:00
_queuedCacheDeletions . Add ( region ) ;
2020-07-11 23:09:37 +10:00
region . Shutdown ( ) ;
}
2020-08-13 14:40:27 +02:00
2020-07-11 23:09:37 +10:00
_regions [ chunk . GridId ] . Remove ( chunk ) ;
}
// Temporarily store the corresponding region for each node
// Makes merging regions or adding nodes to existing regions neater.
var nodeRegions = new Dictionary < PathfindingNode , PathfindingRegion > ( ) ;
var chunkRegions = new HashSet < PathfindingRegion > ( ) ;
_regions [ chunk . GridId ] . Add ( chunk , chunkRegions ) ;
for ( var y = 0 ; y < PathfindingChunk . ChunkSize ; y + + )
{
for ( var x = 0 ; x < PathfindingChunk . ChunkSize ; x + + )
{
var node = chunk . Nodes [ x , y ] ;
2020-08-01 22:13:44 +10:00
var region = CalculateNode ( node , nodeRegions , chunkRegions , x , y ) ;
2020-07-11 23:09:37 +10:00
// Currently we won't store a separate region for each mask / space / whatever because muh effort
// Long-term you'll want to account for it probably
if ( region = = null )
{
continue ;
}
2020-08-06 00:19:00 +10:00
2020-07-11 23:09:37 +10:00
chunkRegions . Add ( region ) ;
}
}
#if DEBUG
2020-08-06 00:19:00 +10:00
foreach ( var region in chunkRegions )
{
DebugTools . Assert ( ! region . Deleted ) ;
}
2020-08-13 14:40:27 +02:00
2020-08-06 00:19:00 +10:00
DebugTools . Assert ( chunkRegions . Count < Math . Pow ( PathfindingChunk . ChunkSize , 2 ) ) ;
2020-07-11 23:09:37 +10:00
SendRegionsDebugMessage ( chunk . GridId ) ;
#endif
}
2021-06-29 15:56:07 +02:00
public void Reset ( RoundRestartCleanupEvent ev )
2020-10-14 22:45:53 +02:00
{
_queuedUpdates . Clear ( ) ;
_regions . Clear ( ) ;
_cachedAccessible . Clear ( ) ;
_queuedCacheDeletions . Clear ( ) ;
}
2020-07-11 23:09:37 +10:00
#if DEBUG
2022-06-11 18:54:41 -07:00
private void SendRegionsDebugMessage ( EntityUid gridId )
2020-07-11 23:09:37 +10:00
{
2021-03-08 04:09:59 +11:00
if ( _subscribedSessions . Count = = 0 ) return ;
2020-08-24 14:10:28 +02:00
var grid = _mapManager . GetGrid ( gridId ) ;
2020-07-11 23:09:37 +10:00
// Chunk / Regions / Nodes
var debugResult = new Dictionary < int , Dictionary < int , List < Vector2 > > > ( ) ;
var chunkIdx = 0 ;
var regionIdx = 0 ;
2020-07-17 11:17:42 +02:00
if ( ! _regions . TryGetValue ( gridId , out var dict ) )
{
return ;
}
foreach ( var ( _ , regions ) in dict )
2020-07-11 23:09:37 +10:00
{
var debugRegions = new Dictionary < int , List < Vector2 > > ( ) ;
debugResult . Add ( chunkIdx , debugRegions ) ;
foreach ( var region in regions )
{
var debugRegionNodes = new List < Vector2 > ( region . Nodes . Count ) ;
debugResult [ chunkIdx ] . Add ( regionIdx , debugRegionNodes ) ;
foreach ( var node in region . Nodes )
{
2020-11-11 01:48:54 +01:00
var nodeVector = grid . GridTileToLocal ( node . TileRef . GridIndices ) . ToMapPos ( EntityManager ) ;
2020-07-11 23:09:37 +10:00
debugRegionNodes . Add ( nodeVector ) ;
}
regionIdx + + ;
}
chunkIdx + + ;
}
2021-03-08 04:09:59 +11:00
foreach ( var session in _subscribedSessions )
{
RaiseNetworkEvent ( new SharedAiDebug . ReachableChunkRegionsDebugMessage ( gridId , debugResult ) , session . ConnectedClient ) ;
}
2020-07-11 23:09:37 +10:00
}
/// <summary>
2021-03-08 04:09:59 +11:00
/// Sent whenever the reachable cache for a particular mob is built or retrieved
2020-07-11 23:09:37 +10:00
/// </summary>
/// <param name="gridId"></param>
/// <param name="regions"></param>
/// <param name="cached"></param>
2022-06-11 18:54:41 -07:00
private void SendRegionCacheMessage ( EntityUid gridId , IEnumerable < PathfindingRegion > regions , bool cached )
2020-07-11 23:09:37 +10:00
{
2021-03-08 04:09:59 +11:00
if ( _subscribedSessions . Count = = 0 ) return ;
2020-08-24 14:10:28 +02:00
var grid = _mapManager . GetGrid ( gridId ) ;
2020-07-11 23:09:37 +10:00
var debugResult = new Dictionary < int , List < Vector2 > > ( ) ;
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
foreach ( var region in regions )
{
debugResult . Add ( _runningCacheIdx , new List < Vector2 > ( ) ) ;
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
foreach ( var node in region . Nodes )
{
2020-11-11 01:48:54 +01:00
var nodeVector = grid . GridTileToLocal ( node . TileRef . GridIndices ) . ToMapPos ( EntityManager ) ;
2020-07-17 11:17:42 +02:00
2020-07-11 23:09:37 +10:00
debugResult [ _runningCacheIdx ] . Add ( nodeVector ) ;
}
_runningCacheIdx + + ;
}
2021-03-08 04:09:59 +11:00
foreach ( var session in _subscribedSessions )
{
RaiseNetworkEvent ( new SharedAiDebug . ReachableCacheDebugMessage ( gridId , debugResult , cached ) , session . ConnectedClient ) ;
}
2020-07-11 23:09:37 +10:00
}
#endif
}
2020-07-17 11:17:42 +02:00
}