--- /dev/null
+using System.Numerics;
+using Content.Shared.Silicons.StationAi;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Enums;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Silicons.StationAi;
+
+public sealed class StationAiOverlay : Overlay
+{
+ [Dependency] private readonly IClyde _clyde = default!;
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+
+ private readonly HashSet<Vector2i> _visibleTiles = new();
+
+ private IRenderTexture? _staticTexture;
+ private IRenderTexture? _stencilTexture;
+
+ public StationAiOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (_stencilTexture?.Texture.Size != args.Viewport.Size)
+ {
+ _staticTexture?.Dispose();
+ _stencilTexture?.Dispose();
+ _stencilTexture = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "station-ai-stencil");
+ _staticTexture = _clyde.CreateRenderTarget(args.Viewport.Size,
+ new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
+ name: "station-ai-static");
+ }
+
+ var worldHandle = args.WorldHandle;
+
+ var worldBounds = args.WorldBounds;
+
+ var playerEnt = _player.LocalEntity;
+ _entManager.TryGetComponent(playerEnt, out TransformComponent? playerXform);
+ var gridUid = playerXform?.GridUid ?? EntityUid.Invalid;
+ _entManager.TryGetComponent(gridUid, out MapGridComponent? grid);
+
+ var invMatrix = args.Viewport.GetWorldToLocalMatrix();
+
+ if (grid != null)
+ {
+ // TODO: Pass in attached entity's grid.
+ // TODO: Credit OD on the moved to code
+ // TODO: Call the moved-to code here.
+
+ _visibleTiles.Clear();
+ var lookups = _entManager.System<EntityLookupSystem>();
+ var xforms = _entManager.System<SharedTransformSystem>();
+ _entManager.System<StationAiVisionSystem>().GetView((gridUid, grid), worldBounds, _visibleTiles);
+
+ var gridMatrix = xforms.GetWorldMatrix(gridUid);
+ var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
+
+ // Draw visible tiles to stencil
+ worldHandle.RenderInRenderTarget(_stencilTexture!, () =>
+ {
+ worldHandle.SetTransform(matty);
+
+ foreach (var tile in _visibleTiles)
+ {
+ var aabb = lookups.GetLocalBounds(tile, grid.TileSize);
+ worldHandle.DrawRect(aabb, Color.White);
+ }
+ },
+ Color.Transparent);
+
+ // Once this is gucci optimise rendering.
+ worldHandle.RenderInRenderTarget(_staticTexture!,
+ () =>
+ {
+ worldHandle.SetTransform(invMatrix);
+ var shader = _proto.Index<ShaderPrototype>("CameraStatic").Instance();
+ worldHandle.UseShader(shader);
+ worldHandle.DrawRect(worldBounds, Color.White);
+ },
+ Color.Black);
+ }
+ // Not on a grid
+ else
+ {
+ worldHandle.RenderInRenderTarget(_stencilTexture!, () =>
+ {
+ },
+ Color.Transparent);
+
+ worldHandle.RenderInRenderTarget(_staticTexture!,
+ () =>
+ {
+ worldHandle.SetTransform(Matrix3x2.Identity);
+ worldHandle.DrawRect(worldBounds, Color.Black);
+ }, Color.Black);
+ }
+
+ // Use the lighting as a mask
+ worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilMask").Instance());
+ worldHandle.DrawTextureRect(_stencilTexture!.Texture, worldBounds);
+
+ // Draw the static
+ worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilDraw").Instance());
+ worldHandle.DrawTextureRect(_staticTexture!.Texture, worldBounds);
+
+ worldHandle.SetTransform(Matrix3x2.Identity);
+ worldHandle.UseShader(null);
+
+ }
+}
--- /dev/null
+using Robust.Shared.Map.Components;
+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();
+
+ // 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;
+
+ /// <summary>
+ /// Have we found the target tile if we're only checking for a single one.
+ /// </summary>
+ private bool TargetFound;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _seedJob = new()
+ {
+ System = this,
+ };
+
+ _job = new ViewJob()
+ {
+ EntManager = EntityManager,
+ Maps = _maps,
+ System = this,
+ };
+ }
+
+ /// <summary>
+ /// Returns whether a tile is accessible based on vision.
+ /// </summary>
+ public bool IsAccessible(Entity<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.Comp.TileSize);
+ var expandedBounds = localBounds.Enlarged(expansionSize);
+
+ _seedJob.Grid = grid;
+ _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))
+ {
+ var tileBounds = _lookup.GetLocalBounds(tileRef.GridIndices, grid.Comp.TileSize).Enlarged(-0.05f);
+
+ _occluders.Clear();
+ _lookup.GetLocalEntitiesIntersecting(grid.Owner, tileBounds, _occluders, LookupFlags.Static);
+
+ if (_occluders.Count > 0)
+ {
+ _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>());
+ }
+
+ _job.TargetTile = tile;
+ TargetFound = false;
+ _singleTiles.Clear();
+ _job.Grid = grid;
+ _job.VisibleTiles = _singleTiles;
+ _parallel.ProcessNow(_job, _job.Data.Count);
+
+ return TargetFound;
+ }
+
+ /// <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<MapGridComponent> grid, Box2Rotated worldBounds, HashSet<Vector2i> visibleTiles, float expansionSize = 8.5f)
+ {
+ _viewportTiles.Clear();
+ _opaque.Clear();
+ _seeds.Clear();
+ var expandedBounds = worldBounds.Enlarged(expansionSize);
+
+ // TODO: Would be nice to be able to run this while running the other stuff.
+ _seedJob.Grid = grid;
+ var localAABB = _xforms.GetInvWorldMatrix(grid).TransformBox(expandedBounds);
+ _seedJob.ExpandedBounds = localAABB;
+ _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))
+ {
+ var tileBounds = _lookup.GetLocalBounds(tileRef.GridIndices, grid.Comp.TileSize).Enlarged(-0.05f);
+
+ _occluders.Clear();
+ _lookup.GetLocalEntitiesIntersecting(grid.Owner, tileBounds, _occluders, LookupFlags.Static);
+
+ if (_occluders.Count > 0)
+ {
+ _opaque.Add(tileRef.GridIndices);
+ }
+
+ _viewportTiles.Add(tileRef.GridIndices);
+ }
+
+ tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, localAABB, ignoreEmpty: false);
+
+ // Get all other relevant tiles.
+ while (tileEnumerator.MoveNext(out var tileRef))
+ {
+ if (_viewportTiles.Contains(tileRef.GridIndices))
+ continue;
+
+ var tileBounds = _lookup.GetLocalBounds(tileRef.GridIndices, grid.Comp.TileSize).Enlarged(-0.05f);
+
+ _occluders.Clear();
+ _lookup.GetLocalEntitiesIntersecting(grid.Owner, tileBounds, _occluders, LookupFlags.Static);
+
+ if (_occluders.Count > 0)
+ {
+ _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.TargetTile = null;
+ TargetFound = false;
+ _job.Grid = grid;
+ _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;
+ }
+
+ /// 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()
+ {
+ System._lookup.GetLocalEntitiesIntersecting(Grid.Owner, ExpandedBounds, System._seeds);
+ }
+ }
+
+ 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();
+
+ // If we're doing range-checks might be able to early out
+ public Vector2i? TargetTile;
+
+ 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)
+ {
+ // If we're looking for a single tile then early-out if someone else has found it.
+ if (TargetTile != null)
+ {
+ lock (System)
+ {
+ if (System.TargetFound)
+ {
+ return;
+ }
+ }
+ }
+
+ 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);
+
+ // Try to find the target tile.
+ if (TargetTile != null)
+ {
+ foreach (var tile in squircles)
+ {
+ if (tile.GridIndices == TargetTile)
+ {
+ lock (System)
+ {
+ System.TargetFound = true;
+ }
+
+ return;
+ }
+ }
+ }
+ else
+ {
+ 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;
+ }
+
+ if (TargetTile != null)
+ {
+ if (vis2.TryGetValue(TargetTile.Value, out var tileVis2))
+ {
+ DebugTools.Assert(seedTiles.Contains(TargetTile.Value));
+
+ if (tileVis2 != 0)
+ {
+ lock (System)
+ {
+ System.TargetFound = true;
+ return;
+ }
+ }
+ }
+ }
+ else
+ {
+ // 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 tileVis2 = vis2.GetValueOrDefault(tile, 0);
+
+ if (tileVis2 != 0)
+ {
+ // No idea if it's better to do this inside or out.
+ lock (VisibleTiles)
+ {
+ VisibleTiles.Add(tile);
+ }
+ }
+ }
+ }
+ }
+ }
+}