| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468 |
- using Content.Shared.StationAi;
- using Robust.Shared.Map.Components;
- using Robust.Shared.Physics;
- 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();
- private EntityQuery<OccluderComponent> _occluderQuery;
- // 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();
- _occluderQuery = GetEntityQuery<OccluderComponent>();
- _seedJob = new()
- {
- System = this,
- };
- _job = new ViewJob()
- {
- EntManager = EntityManager,
- Maps = _maps,
- System = this,
- VisibleTiles = _singleTiles,
- };
- }
- /// <summary>
- /// Returns whether a tile is accessible based on vision.
- /// </summary>
- public bool IsAccessible(Entity<BroadphaseComponent, MapGridComponent> grid, Vector2i tile, float expansionSize = 8.5f, bool fastPath = false)
- {
- _viewportTiles.Clear();
- _opaque.Clear();
- _seeds.Clear();
- _viewportTiles.Add(tile);
- var localBounds = _lookup.GetLocalBounds(tile, grid.Comp2.TileSize);
- var expandedBounds = localBounds.Enlarged(expansionSize);
- _seedJob.Grid = (grid.Owner, grid.Comp2);
- _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))
- {
- if (IsOccluded(grid, tileRef.GridIndices))
- {
- _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();
- _job.Grid = (grid.Owner, grid.Comp2);
- _job.VisibleTiles = _singleTiles;
- _parallel.ProcessNow(_job, _job.Data.Count);
- return _job.VisibleTiles.Contains(tile);
- }
- private bool IsOccluded(Entity<BroadphaseComponent, MapGridComponent> grid, Vector2i tile)
- {
- var tileBounds = _lookup.GetLocalBounds(tile, grid.Comp2.TileSize).Enlarged(-0.05f);
- _occluders.Clear();
- _lookup.GetLocalEntitiesIntersecting((grid.Owner, grid.Comp1), tileBounds, _occluders, query: _occluderQuery, flags: LookupFlags.Static | LookupFlags.Approximate);
- var anyOccluders = false;
- foreach (var occluder in _occluders)
- {
- if (!occluder.Comp.Enabled)
- continue;
- anyOccluders = true;
- break;
- }
- return anyOccluders;
- }
- /// <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>
- public void GetView(Entity<BroadphaseComponent, MapGridComponent> grid, Box2Rotated worldBounds, HashSet<Vector2i> visibleTiles, float expansionSize = 8.5f)
- {
- _viewportTiles.Clear();
- _opaque.Clear();
- _seeds.Clear();
- // TODO: Would be nice to be able to run this while running the other stuff.
- _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;
- _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
- var tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, localAabb, ignoreEmpty: false);
- while (tileEnumerator.MoveNext(out var tileRef))
- {
- if (IsOccluded(grid, tileRef.GridIndices))
- {
- _opaque.Add(tileRef.GridIndices);
- }
- _viewportTiles.Add(tileRef.GridIndices);
- }
- tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, enlargedLocalAabb, ignoreEmpty: false);
- while (tileEnumerator.MoveNext(out var tileRef))
- {
- if (_viewportTiles.Contains(tileRef.GridIndices))
- continue;
- if (IsOccluded(grid, tileRef.GridIndices))
- {
- _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>());
- }
- _job.Grid = (grid.Owner, grid.Comp2);
- _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;
- }
- /// <summary>
- /// 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 required StationAiVisionSystem System;
- public Entity<MapGridComponent> Grid;
- public Box2 ExpandedBounds;
- public void Execute()
- {
- System._lookup.GetLocalEntitiesIntersecting(Grid.Owner, ExpandedBounds, System._seeds, flags: LookupFlags.All | LookupFlags.Approximate);
- }
- }
- private record struct ViewJob() : IParallelRobustJob
- {
- public int BatchSize => 1;
- public required IEntityManager EntManager;
- public required SharedMapSystem Maps;
- public required StationAiVisionSystem System;
- public Entity<MapGridComponent> Grid;
- public List<Entity<StationAiVisionComponent>> Data = new();
- public required 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);
- lock (VisibleTiles)
- {
- foreach (var tile in squircles)
- {
- VisibleTiles.Add(tile.GridIndices);
- }
- }
- 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;
- }
- // vis2 is what we care about for LOS.
- foreach (var tile in seedTiles)
- {
- // If not in viewport don't care.
- if (!System._viewportTiles.Contains(tile))
- continue;
- var tileVis = vis1.GetValueOrDefault(tile, 0);
- if (tileVis != 0)
- {
- // No idea if it's better to do this inside or out.
- lock (VisibleTiles)
- {
- VisibleTiles.Add(tile);
- }
- }
- }
- }
- }
- }
|