2024-08-28 10:57:12 +10:00
using Content.Shared.StationAi ;
2024-08-23 15:13:47 +10:00
using Robust.Shared.Map.Components ;
2024-08-28 10:57:12 +10:00
using Robust.Shared.Physics ;
2024-08-23 15:13:47 +10:00
using Robust.Shared.Threading ;
using Robust.Shared.Utility ;
namespace Content.Shared.Silicons.StationAi ;
public sealed class StationAiVisionSystem : EntitySystem
{
/ *
* This class handles 2 things :
* 1. It handles general "what tiles are visible" line of sight checks .
* 2. It does single - tile lookups to tell if they ' re visible or not with support for a faster range - only path .
* /
[Dependency] private readonly IParallelManager _parallel = default ! ;
[Dependency] private readonly EntityLookupSystem _lookup = default ! ;
[Dependency] private readonly SharedMapSystem _maps = default ! ;
[Dependency] private readonly SharedTransformSystem _xforms = default ! ;
private SeedJob _seedJob ;
private ViewJob _job ;
private readonly HashSet < Entity < OccluderComponent > > _occluders = new ( ) ;
private readonly HashSet < Entity < StationAiVisionComponent > > _seeds = new ( ) ;
private readonly HashSet < Vector2i > _viewportTiles = new ( ) ;
2024-08-28 10:57:12 +10:00
private EntityQuery < OccluderComponent > _occluderQuery ;
2024-08-23 15:13:47 +10:00
// Dummy set
private readonly HashSet < Vector2i > _singleTiles = new ( ) ;
// Occupied tiles per-run.
// For now it's only 1-grid supported but updating to TileRefs if required shouldn't be too hard.
private readonly HashSet < Vector2i > _opaque = new ( ) ;
/// <summary>
/// Do we skip line of sight checks and just check vision ranges.
/// </summary>
private bool FastPath ;
public override void Initialize ( )
{
base . Initialize ( ) ;
2024-08-28 10:57:12 +10:00
_occluderQuery = GetEntityQuery < OccluderComponent > ( ) ;
2024-08-23 15:13:47 +10:00
_seedJob = new ( )
{
System = this ,
} ;
_job = new ViewJob ( )
{
EntManager = EntityManager ,
Maps = _maps ,
System = this ,
} ;
}
/// <summary>
/// Returns whether a tile is accessible based on vision.
/// </summary>
2024-08-28 10:57:12 +10:00
public bool IsAccessible ( Entity < BroadphaseComponent , MapGridComponent > grid , Vector2i tile , float expansionSize = 8.5f , bool fastPath = false )
2024-08-23 15:13:47 +10:00
{
_viewportTiles . Clear ( ) ;
_opaque . Clear ( ) ;
_seeds . Clear ( ) ;
_viewportTiles . Add ( tile ) ;
2024-08-28 10:57:12 +10:00
var localBounds = _lookup . GetLocalBounds ( tile , grid . Comp2 . TileSize ) ;
2024-08-23 15:13:47 +10:00
var expandedBounds = localBounds . Enlarged ( expansionSize ) ;
2024-08-28 10:57:12 +10:00
_seedJob . Grid = ( grid . Owner , grid . Comp2 ) ;
2024-08-23 15:13:47 +10:00
_seedJob . ExpandedBounds = expandedBounds ;
_parallel . ProcessNow ( _seedJob ) ;
_job . Data . Clear ( ) ;
FastPath = fastPath ;
foreach ( var seed in _seeds )
{
if ( ! seed . Comp . Enabled )
continue ;
_job . Data . Add ( seed ) ;
}
if ( _seeds . Count = = 0 )
return false ;
// Skip occluders step if we're just doing range checks.
if ( ! fastPath )
{
var tileEnumerator = _maps . GetLocalTilesEnumerator ( grid , grid , expandedBounds , ignoreEmpty : false ) ;
// Get all other relevant tiles.
while ( tileEnumerator . MoveNext ( out var tileRef ) )
{
2024-08-23 15:57:57 +10:00
if ( IsOccluded ( grid , tileRef . GridIndices ) )
2024-08-23 15:13:47 +10:00
{
_opaque . Add ( tileRef . GridIndices ) ;
}
}
}
for ( var i = _job . Vis1 . Count ; i < _job . Data . Count ; i + + )
{
_job . Vis1 . Add ( new Dictionary < Vector2i , int > ( ) ) ;
_job . Vis2 . Add ( new Dictionary < Vector2i , int > ( ) ) ;
_job . SeedTiles . Add ( new HashSet < Vector2i > ( ) ) ;
_job . BoundaryTiles . Add ( new HashSet < Vector2i > ( ) ) ;
}
_singleTiles . Clear ( ) ;
2024-08-28 10:57:12 +10:00
_job . Grid = ( grid . Owner , grid . Comp2 ) ;
2024-08-23 15:13:47 +10:00
_job . VisibleTiles = _singleTiles ;
_parallel . ProcessNow ( _job , _job . Data . Count ) ;
2024-08-28 10:57:12 +10:00
return _job . VisibleTiles . Contains ( tile ) ;
2024-08-23 15:13:47 +10:00
}
2024-08-28 10:57:12 +10:00
private bool IsOccluded ( Entity < BroadphaseComponent , MapGridComponent > grid , Vector2i tile )
2024-08-23 15:57:57 +10:00
{
2024-08-28 10:57:12 +10:00
var tileBounds = _lookup . GetLocalBounds ( tile , grid . Comp2 . TileSize ) . Enlarged ( - 0.05f ) ;
2024-08-23 15:57:57 +10:00
_occluders . Clear ( ) ;
2024-08-28 10:57:12 +10:00
_lookup . GetLocalEntitiesIntersecting ( ( grid . Owner , grid . Comp1 ) , tileBounds , _occluders , query : _occluderQuery , flags : LookupFlags . Static | LookupFlags . Approximate ) ;
2024-08-23 15:57:57 +10:00
var anyOccluders = false ;
foreach ( var occluder in _occluders )
{
if ( ! occluder . Comp . Enabled )
continue ;
anyOccluders = true ;
break ;
}
return anyOccluders ;
}
2024-08-23 15:13:47 +10:00
/// <summary>
/// Gets a byond-equivalent for tiles in the specified worldAABB.
/// </summary>
/// <param name="expansionSize">How much to expand the bounds before to find vision intersecting it. Makes this the largest vision size + 1 tile.</param>
2024-08-28 10:57:12 +10:00
public void GetView ( Entity < BroadphaseComponent , MapGridComponent > grid , Box2Rotated worldBounds , HashSet < Vector2i > visibleTiles , float expansionSize = 8.5f )
2024-08-23 15:13:47 +10:00
{
_viewportTiles . Clear ( ) ;
_opaque . Clear ( ) ;
_seeds . Clear ( ) ;
// TODO: Would be nice to be able to run this while running the other stuff.
2024-08-28 10:57:12 +10:00
_seedJob . Grid = ( grid . Owner , grid . Comp2 ) ;
var invMatrix = _xforms . GetInvWorldMatrix ( grid ) ;
var localAabb = invMatrix . TransformBox ( worldBounds ) ;
var enlargedLocalAabb = invMatrix . TransformBox ( worldBounds . Enlarged ( expansionSize ) ) ;
_seedJob . ExpandedBounds = enlargedLocalAabb ;
2024-08-23 15:13:47 +10:00
_parallel . ProcessNow ( _seedJob ) ;
_job . Data . Clear ( ) ;
FastPath = false ;
foreach ( var seed in _seeds )
{
if ( ! seed . Comp . Enabled )
continue ;
_job . Data . Add ( seed ) ;
}
if ( _seeds . Count = = 0 )
return ;
// Get viewport tiles
2024-08-28 10:57:12 +10:00
var tileEnumerator = _maps . GetLocalTilesEnumerator ( grid , grid , localAabb , ignoreEmpty : false ) ;
2024-08-23 15:13:47 +10:00
while ( tileEnumerator . MoveNext ( out var tileRef ) )
{
2024-08-23 15:57:57 +10:00
if ( IsOccluded ( grid , tileRef . GridIndices ) )
2024-08-23 15:13:47 +10:00
{
_opaque . Add ( tileRef . GridIndices ) ;
}
_viewportTiles . Add ( tileRef . GridIndices ) ;
}
2024-08-28 10:57:12 +10:00
tileEnumerator = _maps . GetLocalTilesEnumerator ( grid , grid , enlargedLocalAabb , ignoreEmpty : false ) ;
2024-08-23 15:13:47 +10:00
while ( tileEnumerator . MoveNext ( out var tileRef ) )
{
if ( _viewportTiles . Contains ( tileRef . GridIndices ) )
continue ;
2024-08-23 15:57:57 +10:00
if ( IsOccluded ( grid , tileRef . GridIndices ) )
2024-08-23 15:13:47 +10:00
{
_opaque . Add ( tileRef . GridIndices ) ;
}
}
// Wait for seed job here
for ( var i = _job . Vis1 . Count ; i < _job . Data . Count ; i + + )
{
_job . Vis1 . Add ( new Dictionary < Vector2i , int > ( ) ) ;
_job . Vis2 . Add ( new Dictionary < Vector2i , int > ( ) ) ;
_job . SeedTiles . Add ( new HashSet < Vector2i > ( ) ) ;
_job . BoundaryTiles . Add ( new HashSet < Vector2i > ( ) ) ;
}
2024-08-28 10:57:12 +10:00
_job . Grid = ( grid . Owner , grid . Comp2 ) ;
2024-08-23 15:13:47 +10:00
_job . VisibleTiles = visibleTiles ;
_parallel . ProcessNow ( _job , _job . Data . Count ) ;
}
private int GetMaxDelta ( Vector2i tile , Vector2i center )
{
var delta = tile - center ;
return Math . Max ( Math . Abs ( delta . X ) , Math . Abs ( delta . Y ) ) ;
}
private int GetSumDelta ( Vector2i tile , Vector2i center )
{
var delta = tile - center ;
return Math . Abs ( delta . X ) + Math . Abs ( delta . Y ) ;
}
/// <summary>
/// Checks if any of a tile's neighbors are visible.
/// </summary>
private bool CheckNeighborsVis (
Dictionary < Vector2i , int > vis ,
Vector2i index ,
int d )
{
for ( var x = - 1 ; x < = 1 ; x + + )
{
for ( var y = - 1 ; y < = 1 ; y + + )
{
if ( x = = 0 & & y = = 0 )
continue ;
var neighbor = index + new Vector2i ( x , y ) ;
var neighborD = vis . GetValueOrDefault ( neighbor ) ;
if ( neighborD = = d )
return true ;
}
}
return false ;
}
2024-08-28 10:57:12 +10:00
/// <summary>
2024-08-23 15:13:47 +10:00
/// Checks whether this tile fits the definition of a "corner"
/// </summary>
private bool IsCorner (
HashSet < Vector2i > tiles ,
HashSet < Vector2i > blocked ,
Dictionary < Vector2i , int > vis1 ,
Vector2i index ,
Vector2i delta )
{
var diagonalIndex = index + delta ;
if ( ! tiles . TryGetValue ( diagonalIndex , out var diagonal ) )
return false ;
var cardinal1 = new Vector2i ( index . X , diagonal . Y ) ;
var cardinal2 = new Vector2i ( diagonal . X , index . Y ) ;
return vis1 . GetValueOrDefault ( diagonal ) ! = 0 & &
vis1 . GetValueOrDefault ( cardinal1 ) ! = 0 & &
vis1 . GetValueOrDefault ( cardinal2 ) ! = 0 & &
blocked . Contains ( cardinal1 ) & &
blocked . Contains ( cardinal2 ) & &
! blocked . Contains ( diagonal ) ;
}
/// <summary>
/// Gets the relevant vision seeds for later.
/// </summary>
private record struct SeedJob ( ) : IRobustJob
{
public StationAiVisionSystem System ;
public Entity < MapGridComponent > Grid ;
public Box2 ExpandedBounds ;
public void Execute ( )
{
2024-08-28 10:57:12 +10:00
System . _lookup . GetLocalEntitiesIntersecting ( Grid . Owner , ExpandedBounds , System . _seeds , flags : LookupFlags . All | LookupFlags . Approximate ) ;
2024-08-23 15:13:47 +10:00
}
}
private record struct ViewJob ( ) : IParallelRobustJob
{
public int BatchSize = > 1 ;
public IEntityManager EntManager ;
public SharedMapSystem Maps ;
public StationAiVisionSystem System ;
public Entity < MapGridComponent > Grid ;
public List < Entity < StationAiVisionComponent > > Data = new ( ) ;
public HashSet < Vector2i > VisibleTiles ;
public readonly List < Dictionary < Vector2i , int > > Vis1 = new ( ) ;
public readonly List < Dictionary < Vector2i , int > > Vis2 = new ( ) ;
public readonly List < HashSet < Vector2i > > SeedTiles = new ( ) ;
public readonly List < HashSet < Vector2i > > BoundaryTiles = new ( ) ;
public void Execute ( int index )
{
var seed = Data [ index ] ;
var seedXform = EntManager . GetComponent < TransformComponent > ( seed ) ;
// Fastpath just get tiles in range.
// Either xray-vision or system is doing a quick-and-dirty check.
if ( ! seed . Comp . Occluded | | System . FastPath )
{
var squircles = Maps . GetLocalTilesIntersecting ( Grid . Owner ,
Grid . Comp ,
new Circle ( System . _xforms . GetWorldPosition ( seedXform ) , seed . Comp . Range ) , ignoreEmpty : false ) ;
2024-08-28 10:57:12 +10:00
lock ( VisibleTiles )
2024-08-23 15:13:47 +10:00
{
foreach ( var tile in squircles )
{
2024-08-28 10:57:12 +10:00
VisibleTiles . Add ( tile . GridIndices ) ;
2024-08-23 15:13:47 +10:00
}
}
return ;
}
// Code based upon https://github.com/OpenDreamProject/OpenDream/blob/c4a3828ccb997bf3722673620460ebb11b95ccdf/OpenDreamShared/Dream/ViewAlgorithm.cs
var range = seed . Comp . Range ;
var vis1 = Vis1 [ index ] ;
var vis2 = Vis2 [ index ] ;
var seedTiles = SeedTiles [ index ] ;
var boundary = BoundaryTiles [ index ] ;
// Cleanup last run
vis1 . Clear ( ) ;
vis2 . Clear ( ) ;
seedTiles . Clear ( ) ;
boundary . Clear ( ) ;
var maxDepthMax = 0 ;
var sumDepthMax = 0 ;
var eyePos = Maps . GetTileRef ( Grid . Owner , Grid , seedXform . Coordinates ) . GridIndices ;
for ( var x = Math . Floor ( eyePos . X - range ) ; x < = eyePos . X + range ; x + + )
{
for ( var y = Math . Floor ( eyePos . Y - range ) ; y < = eyePos . Y + range ; y + + )
{
var tile = new Vector2i ( ( int ) x , ( int ) y ) ;
var delta = tile - eyePos ;
var xDelta = Math . Abs ( delta . X ) ;
var yDelta = Math . Abs ( delta . Y ) ;
var deltaSum = xDelta + yDelta ;
maxDepthMax = Math . Max ( maxDepthMax , Math . Max ( xDelta , yDelta ) ) ;
sumDepthMax = Math . Max ( sumDepthMax , deltaSum ) ;
seedTiles . Add ( tile ) ;
}
}
// Step 3, Diagonal shadow loop
for ( var d = 0 ; d < maxDepthMax ; d + + )
{
foreach ( var tile in seedTiles )
{
var maxDelta = System . GetMaxDelta ( tile , eyePos ) ;
if ( maxDelta = = d + 1 & & System . CheckNeighborsVis ( vis2 , tile , d ) )
{
vis2 [ tile ] = ( System . _opaque . Contains ( tile ) ? - 1 : d + 1 ) ;
}
}
}
// Step 4, Straight shadow loop
for ( var d = 0 ; d < sumDepthMax ; d + + )
{
foreach ( var tile in seedTiles )
{
var sumDelta = System . GetSumDelta ( tile , eyePos ) ;
if ( sumDelta = = d + 1 & & System . CheckNeighborsVis ( vis1 , tile , d ) )
{
if ( System . _opaque . Contains ( tile ) )
{
vis1 [ tile ] = - 1 ;
}
else if ( vis2 . GetValueOrDefault ( tile ) ! = 0 )
{
vis1 [ tile ] = d + 1 ;
}
}
}
}
// Add the eye itself
vis1 [ eyePos ] = 1 ;
// Step 6.
// Step 7.
// Step 8.
foreach ( var tile in seedTiles )
{
vis2 [ tile ] = vis1 . GetValueOrDefault ( tile , 0 ) ;
}
// Step 9
foreach ( var tile in seedTiles )
{
if ( ! System . _opaque . Contains ( tile ) )
continue ;
var tileVis1 = vis1 . GetValueOrDefault ( tile ) ;
if ( tileVis1 ! = 0 )
continue ;
if ( System . IsCorner ( seedTiles , System . _opaque , vis1 , tile , Vector2i . UpRight ) | |
System . IsCorner ( seedTiles , System . _opaque , vis1 , tile , Vector2i . UpLeft ) | |
System . IsCorner ( seedTiles , System . _opaque , vis1 , tile , Vector2i . DownLeft ) | |
System . IsCorner ( seedTiles , System . _opaque , vis1 , tile , Vector2i . DownRight ) )
{
boundary . Add ( tile ) ;
}
}
// Make all wall/corner tiles visible
foreach ( var tile in boundary )
{
vis1 [ tile ] = - 1 ;
}
2024-08-28 10:57:12 +10:00
// vis2 is what we care about for LOS.
foreach ( var tile in seedTiles )
2024-08-23 15:13:47 +10:00
{
2024-08-28 10:57:12 +10:00
// If not in viewport don't care.
if ( ! System . _viewportTiles . Contains ( tile ) )
continue ;
2024-08-23 15:13:47 +10:00
2024-08-28 10:57:12 +10:00
var tileVis = vis1 . GetValueOrDefault ( tile , 0 ) ;
2024-08-23 15:13:47 +10:00
2024-08-28 10:57:12 +10:00
if ( tileVis ! = 0 )
{
// No idea if it's better to do this inside or out.
lock ( VisibleTiles )
2024-08-23 15:13:47 +10:00
{
2024-08-28 10:57:12 +10:00
VisibleTiles . Add ( tile ) ;
2024-08-23 15:13:47 +10:00
}
}
}
}
}
}