]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Station AI overlay (#31335)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Fri, 23 Aug 2024 05:13:47 +0000 (15:13 +1000)
committerGitHub <noreply@github.com>
Fri, 23 Aug 2024 05:13:47 +0000 (23:13 -0600)
Split

Content.Client/Silicons/StationAi/StationAiOverlay.cs [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiSystem.cs [new file with mode: 0644]
Content.Client/UserInterface/Systems/Sandbox/SandboxUIController.cs
Content.Client/UserInterface/Systems/Sandbox/Windows/SandboxWindow.xaml
Content.Shared/Silicons/StationAi/StationAiOverlayComponent.cs [new file with mode: 0644]
Content.Shared/Silicons/StationAi/StationAiVisionComponent.cs [new file with mode: 0644]
Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/sandbox/sandbox-manager.ftl
Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml

diff --git a/Content.Client/Silicons/StationAi/StationAiOverlay.cs b/Content.Client/Silicons/StationAi/StationAiOverlay.cs
new file mode 100644 (file)
index 0000000..efa1b8d
--- /dev/null
@@ -0,0 +1,119 @@
+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);
+
+    }
+}
diff --git a/Content.Client/Silicons/StationAi/StationAiSystem.cs b/Content.Client/Silicons/StationAi/StationAiSystem.cs
new file mode 100644 (file)
index 0000000..2ed0617
--- /dev/null
@@ -0,0 +1,80 @@
+using Content.Shared.Silicons.StationAi;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Player;
+
+namespace Content.Client.Silicons.StationAi;
+
+public sealed partial class StationAiSystem : EntitySystem
+{
+    [Dependency] private readonly IOverlayManager _overlayMgr = default!;
+    [Dependency] private readonly IPlayerManager _player = default!;
+
+    private StationAiOverlay? _overlay;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        // InitializeAirlock();
+        // InitializePowerToggle();
+
+        SubscribeLocalEvent<StationAiOverlayComponent, LocalPlayerAttachedEvent>(OnAiAttached);
+        SubscribeLocalEvent<StationAiOverlayComponent, LocalPlayerDetachedEvent>(OnAiDetached);
+        SubscribeLocalEvent<StationAiOverlayComponent, ComponentInit>(OnAiOverlayInit);
+        SubscribeLocalEvent<StationAiOverlayComponent, ComponentRemove>(OnAiOverlayRemove);
+    }
+
+    private void OnAiOverlayInit(Entity<StationAiOverlayComponent> ent, ref ComponentInit args)
+    {
+        var attachedEnt = _player.LocalEntity;
+
+        if (attachedEnt != ent.Owner)
+            return;
+
+        AddOverlay();
+    }
+
+    private void OnAiOverlayRemove(Entity<StationAiOverlayComponent> ent, ref ComponentRemove args)
+    {
+        var attachedEnt = _player.LocalEntity;
+
+        if (attachedEnt != ent.Owner)
+            return;
+
+        RemoveOverlay();
+    }
+
+    private void AddOverlay()
+    {
+        if (_overlay != null)
+            return;
+
+        _overlay = new StationAiOverlay();
+        _overlayMgr.AddOverlay(_overlay);
+    }
+
+    private void RemoveOverlay()
+    {
+        if (_overlay == null)
+            return;
+
+        _overlayMgr.RemoveOverlay(_overlay);
+        _overlay = null;
+    }
+
+    private void OnAiAttached(Entity<StationAiOverlayComponent> ent, ref LocalPlayerAttachedEvent args)
+    {
+        AddOverlay();
+    }
+
+    private void OnAiDetached(Entity<StationAiOverlayComponent> ent, ref LocalPlayerDetachedEvent args)
+    {
+        RemoveOverlay();
+    }
+
+    public override void Shutdown()
+    {
+        base.Shutdown();
+        _overlayMgr.RemoveOverlay<StationAiOverlay>();
+    }
+}
index 752c89ca9705bd913459158b4228acd4d61c5f02..58c8a1451bd6c93fa424507ad9b71e3b87731914 100644 (file)
@@ -7,14 +7,17 @@ using Content.Client.UserInterface.Controls;
 using Content.Client.UserInterface.Systems.DecalPlacer;
 using Content.Client.UserInterface.Systems.Sandbox.Windows;
 using Content.Shared.Input;
+using Content.Shared.Silicons.StationAi;
 using JetBrains.Annotations;
 using Robust.Client.Console;
 using Robust.Client.Debugging;
 using Robust.Client.Graphics;
 using Robust.Client.Input;
+using Robust.Client.Player;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controllers;
 using Robust.Client.UserInterface.Controllers.Implementations;
+using Robust.Shared.Console;
 using Robust.Shared.Input.Binding;
 using Robust.Shared.Map;
 using Robust.Shared.Player;
@@ -27,10 +30,12 @@ namespace Content.Client.UserInterface.Systems.Sandbox;
 [UsedImplicitly]
 public sealed class SandboxUIController : UIController, IOnStateChanged<GameplayState>, IOnSystemChanged<SandboxSystem>
 {
+    [Dependency] private readonly IConsoleHost _console = default!;
     [Dependency] private readonly IEyeManager _eye = default!;
     [Dependency] private readonly IInputManager _input = default!;
     [Dependency] private readonly ILightManager _light = default!;
     [Dependency] private readonly IClientAdminManager _admin = default!;
+    [Dependency] private readonly IPlayerManager _player = default!;
 
     [UISystemDependency] private readonly DebugPhysicsSystem _debugPhysics = default!;
     [UISystemDependency] private readonly MarkerSystem _marker = default!;
@@ -116,6 +121,21 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
         _window.ShowMarkersButton.Pressed = _marker.MarkersVisible;
         _window.ShowBbButton.Pressed = (_debugPhysics.Flags & PhysicsDebugFlags.Shapes) != 0x0;
 
+        _window.AiOverlayButton.OnPressed += args =>
+        {
+            var player = _player.LocalEntity;
+
+            if (player == null)
+                return;
+
+            var pnent = EntityManager.GetNetEntity(player.Value);
+
+            // Need NetworkedAddComponent but engine PR.
+            if (args.Button.Pressed)
+                _console.ExecuteCommand($"addcomp {pnent.Id} StationAiOverlay");
+            else
+                _console.ExecuteCommand($"rmcomp {pnent.Id} StationAiOverlay");
+        };
         _window.RespawnButton.OnPressed += _ => _sandbox.Respawn();
         _window.SpawnTilesButton.OnPressed += _ => TileSpawningController.ToggleWindow();
         _window.SpawnEntitiesButton.OnPressed += _ => EntitySpawningController.ToggleWindow();
index 64367ea27affa9ca0a7cbc6d761ee6bcf4657016..05e65cf29c370ce4d94b739d8af3cf85e623b8eb 100644 (file)
@@ -4,6 +4,7 @@
     Title="{Loc sandbox-window-title}"
     Resizable="False">
     <BoxContainer Orientation="Vertical" SeparationOverride="4">
+        <Button Name="AiOverlayButton" Access="Public" Text="{Loc sandbox-window-ai-overlay-button}" ToggleMode="True"/>
         <Button Name="RespawnButton" Access="Public" Text="{Loc sandbox-window-respawn-button}"/>
         <Button Name="SpawnEntitiesButton" Access="Public" Text="{Loc sandbox-window-spawn-entities-button}"/>
         <Button Name="SpawnTilesButton" Access="Public" Text="{Loc sandbox-window-spawn-tiles-button}"/>
diff --git a/Content.Shared/Silicons/StationAi/StationAiOverlayComponent.cs b/Content.Shared/Silicons/StationAi/StationAiOverlayComponent.cs
new file mode 100644 (file)
index 0000000..8416d44
--- /dev/null
@@ -0,0 +1,9 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Silicons.StationAi;
+
+/// <summary>
+/// Handles the static overlay for station AI.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class StationAiOverlayComponent : Component;
diff --git a/Content.Shared/Silicons/StationAi/StationAiVisionComponent.cs b/Content.Shared/Silicons/StationAi/StationAiVisionComponent.cs
new file mode 100644 (file)
index 0000000..94aef8a
--- /dev/null
@@ -0,0 +1,19 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Silicons.StationAi;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]//, Access(typeof(SharedStationAiSystem))]
+public sealed partial class StationAiVisionComponent : Component
+{
+    [DataField, AutoNetworkedField]
+    public bool Enabled = true;
+
+    [DataField, AutoNetworkedField]
+    public bool Occluded = true;
+
+    /// <summary>
+    /// Range in tiles
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float Range = 7.5f;
+}
diff --git a/Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs b/Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs
new file mode 100644 (file)
index 0000000..c1f5d98
--- /dev/null
@@ -0,0 +1,518 @@
+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);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
index b7f4d03451fcf809ffdc6f842173df273a4ca81e..b6f973673234de6cba39851a4a92ee236d76f830 100644 (file)
@@ -1,4 +1,5 @@
 sandbox-window-title = Sandbox Panel
+sandbox-window-ai-overlay-button = AI Overlay
 sandbox-window-respawn-button = Respawn
 sandbox-window-spawn-entities-button = Spawn Entities
 sandbox-window-spawn-tiles-button = Spawn Tiles
index 2ac81b463af47b2a80f9998d12d5d73c2e2d5542..2a96da27604a9d0f8570eb1735e204eaa505e9a9 100644 (file)
@@ -4,6 +4,7 @@
   name: camera
   description: A surveillance camera. It's watching you. Kinda.
   components:
+  - type: StationAiVision
   - type: Clickable
   - type: InteractionOutline
   - type: Construction