119
Content.Client/Silicons/StationAi/StationAiOverlay.cs
Normal file
119
Content.Client/Silicons/StationAi/StationAiOverlay.cs
Normal file
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
80
Content.Client/Silicons/StationAi/StationAiSystem.cs
Normal file
80
Content.Client/Silicons/StationAi/StationAiSystem.cs
Normal file
@@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,14 +7,17 @@ using Content.Client.UserInterface.Controls;
|
|||||||
using Content.Client.UserInterface.Systems.DecalPlacer;
|
using Content.Client.UserInterface.Systems.DecalPlacer;
|
||||||
using Content.Client.UserInterface.Systems.Sandbox.Windows;
|
using Content.Client.UserInterface.Systems.Sandbox.Windows;
|
||||||
using Content.Shared.Input;
|
using Content.Shared.Input;
|
||||||
|
using Content.Shared.Silicons.StationAi;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Client.Console;
|
using Robust.Client.Console;
|
||||||
using Robust.Client.Debugging;
|
using Robust.Client.Debugging;
|
||||||
using Robust.Client.Graphics;
|
using Robust.Client.Graphics;
|
||||||
using Robust.Client.Input;
|
using Robust.Client.Input;
|
||||||
|
using Robust.Client.Player;
|
||||||
using Robust.Client.UserInterface;
|
using Robust.Client.UserInterface;
|
||||||
using Robust.Client.UserInterface.Controllers;
|
using Robust.Client.UserInterface.Controllers;
|
||||||
using Robust.Client.UserInterface.Controllers.Implementations;
|
using Robust.Client.UserInterface.Controllers.Implementations;
|
||||||
|
using Robust.Shared.Console;
|
||||||
using Robust.Shared.Input.Binding;
|
using Robust.Shared.Input.Binding;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
@@ -27,10 +30,12 @@ namespace Content.Client.UserInterface.Systems.Sandbox;
|
|||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public sealed class SandboxUIController : UIController, IOnStateChanged<GameplayState>, IOnSystemChanged<SandboxSystem>
|
public sealed class SandboxUIController : UIController, IOnStateChanged<GameplayState>, IOnSystemChanged<SandboxSystem>
|
||||||
{
|
{
|
||||||
|
[Dependency] private readonly IConsoleHost _console = default!;
|
||||||
[Dependency] private readonly IEyeManager _eye = default!;
|
[Dependency] private readonly IEyeManager _eye = default!;
|
||||||
[Dependency] private readonly IInputManager _input = default!;
|
[Dependency] private readonly IInputManager _input = default!;
|
||||||
[Dependency] private readonly ILightManager _light = default!;
|
[Dependency] private readonly ILightManager _light = default!;
|
||||||
[Dependency] private readonly IClientAdminManager _admin = default!;
|
[Dependency] private readonly IClientAdminManager _admin = default!;
|
||||||
|
[Dependency] private readonly IPlayerManager _player = default!;
|
||||||
|
|
||||||
[UISystemDependency] private readonly DebugPhysicsSystem _debugPhysics = default!;
|
[UISystemDependency] private readonly DebugPhysicsSystem _debugPhysics = default!;
|
||||||
[UISystemDependency] private readonly MarkerSystem _marker = default!;
|
[UISystemDependency] private readonly MarkerSystem _marker = default!;
|
||||||
@@ -116,6 +121,21 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
|||||||
_window.ShowMarkersButton.Pressed = _marker.MarkersVisible;
|
_window.ShowMarkersButton.Pressed = _marker.MarkersVisible;
|
||||||
_window.ShowBbButton.Pressed = (_debugPhysics.Flags & PhysicsDebugFlags.Shapes) != 0x0;
|
_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.RespawnButton.OnPressed += _ => _sandbox.Respawn();
|
||||||
_window.SpawnTilesButton.OnPressed += _ => TileSpawningController.ToggleWindow();
|
_window.SpawnTilesButton.OnPressed += _ => TileSpawningController.ToggleWindow();
|
||||||
_window.SpawnEntitiesButton.OnPressed += _ => EntitySpawningController.ToggleWindow();
|
_window.SpawnEntitiesButton.OnPressed += _ => EntitySpawningController.ToggleWindow();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
Title="{Loc sandbox-window-title}"
|
Title="{Loc sandbox-window-title}"
|
||||||
Resizable="False">
|
Resizable="False">
|
||||||
<BoxContainer Orientation="Vertical" SeparationOverride="4">
|
<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="RespawnButton" Access="Public" Text="{Loc sandbox-window-respawn-button}"/>
|
||||||
<Button Name="SpawnEntitiesButton" Access="Public" Text="{Loc sandbox-window-spawn-entities-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}"/>
|
<Button Name="SpawnTilesButton" Access="Public" Text="{Loc sandbox-window-spawn-tiles-button}"/>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
518
Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs
Normal file
518
Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
sandbox-window-title = Sandbox Panel
|
sandbox-window-title = Sandbox Panel
|
||||||
|
sandbox-window-ai-overlay-button = AI Overlay
|
||||||
sandbox-window-respawn-button = Respawn
|
sandbox-window-respawn-button = Respawn
|
||||||
sandbox-window-spawn-entities-button = Spawn Entities
|
sandbox-window-spawn-entities-button = Spawn Entities
|
||||||
sandbox-window-spawn-tiles-button = Spawn Tiles
|
sandbox-window-spawn-tiles-button = Spawn Tiles
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
name: camera
|
name: camera
|
||||||
description: A surveillance camera. It's watching you. Kinda.
|
description: A surveillance camera. It's watching you. Kinda.
|
||||||
components:
|
components:
|
||||||
|
- type: StationAiVision
|
||||||
- type: Clickable
|
- type: Clickable
|
||||||
- type: InteractionOutline
|
- type: InteractionOutline
|
||||||
- type: Construction
|
- type: Construction
|
||||||
|
|||||||
Reference in New Issue
Block a user