using System.Numerics; using System.Runtime.InteropServices; using Content.Client.Graphics; using Content.Shared.Maps; using Robust.Client.Graphics; using Robust.Shared.Enums; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Timing; namespace Content.Client.Light.EntitySystems; /// /// Builds viewport-sized masks for grid contents that need to be reused by world overlays. /// public sealed partial class GridStencilSystem : EntitySystem { public delegate bool TileStencilPredicate(Entity grid, TileRef tile); [Dependency] private IClyde _clyde = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private EntityLookupSystem _lookup = default!; [Dependency] private SharedMapSystem _map = default!; [Dependency] private SharedTransformSystem _xform = default!; [Dependency] private TurfSystem _turf = default!; private readonly OverlayResourceCache _resources = new(); private List> _grids = new(); private readonly List _rects = new(); /// /// Returns a viewport-sized texture where non-space grid tiles are white and everything else is transparent. /// The texture is rebuilt at most once per viewport per frame. /// public IRenderTexture GetNonSpaceStencil(in OverlayDrawArgs args) { return GetTileBoundsStencil(args, "non-space", "non-space-grid-stencil", tile => !_turf.IsSpace(tile)); } private IRenderTexture GetTileBoundsStencil( in OverlayDrawArgs args, string key, string name, Predicate predicate) { var viewport = args.Viewport; var target = viewport.RenderTarget; var res = _resources.GetForViewport(viewport, static _ => new CachedResources()); var targetRes = res.GetOrCreate(key); if (targetRes.Target?.Texture.Size != target.Size) { targetRes.Target?.Dispose(); targetRes.Target = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: name); targetRes.LastFrame = 0; } var worldHandle = args.WorldHandle; var invMatrix = viewport.GetWorldToLocalMatrix(); var mapId = args.MapId; var worldBounds = args.WorldBounds; if (targetRes.LastFrame == _timing.CurFrame && targetRes.LastMapId == mapId && targetRes.LastWorldBounds.Equals(worldBounds)) { return targetRes.Target!; } targetRes.LastFrame = _timing.CurFrame; targetRes.LastMapId = mapId; targetRes.LastWorldBounds = worldBounds; worldHandle.RenderInRenderTarget(targetRes.Target, () => { _grids.Clear(); _map.FindGridsIntersecting(mapId, worldBounds, ref _grids); foreach (var grid in _grids) { var worldToTextureMatrix = Matrix3x2.Multiply(_xform.GetWorldMatrix(grid.Owner), invMatrix); var tiles = _map.GetTilesIntersecting(grid.Owner, grid, worldBounds); worldHandle.SetTransform(worldToTextureMatrix); _rects.Clear(); while (tiles.MoveNext(out var tileRef)) { if (!predicate(tileRef.Tile)) continue; var bounds = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize); _rects.Add(new WorldRect(bounds, Color.White)); } worldHandle.DrawRectsUnmodulated(CollectionsMarshal.AsSpan(_rects)); } }, Color.Transparent); return targetRes.Target!; } /// /// Returns a viewport-sized texture where tiles accepted by are white and everything else is transparent. /// public IRenderTexture GetTileStencil( in OverlayDrawArgs args, string key, string name, TileStencilPredicate predicate) { var viewport = args.Viewport; var target = viewport.RenderTarget; var res = _resources.GetForViewport(viewport, static _ => new CachedResources()); var targetRes = res.GetOrCreate(key); if (targetRes.Target?.Texture.Size != target.Size) { targetRes.Target?.Dispose(); targetRes.Target = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: name); targetRes.LastFrame = 0; } var worldHandle = args.WorldHandle; var invMatrix = viewport.GetWorldToLocalMatrix(); var mapId = args.MapId; var worldBounds = args.WorldBounds; if (targetRes.LastFrame == _timing.CurFrame && targetRes.LastMapId == mapId && targetRes.LastWorldBounds.Equals(worldBounds)) { return targetRes.Target!; } targetRes.LastFrame = _timing.CurFrame; targetRes.LastMapId = mapId; targetRes.LastWorldBounds = worldBounds; worldHandle.RenderInRenderTarget(targetRes.Target, () => { _grids.Clear(); _map.FindGridsIntersecting(mapId, worldBounds, ref _grids); foreach (var grid in _grids) { var transform = _xform.GetWorldMatrix(grid.Owner); var worldToTextureMatrix = Matrix3x2.Multiply(transform, invMatrix); var tiles = _map.GetTilesIntersecting(grid.Owner, grid, worldBounds); worldHandle.SetTransform(worldToTextureMatrix); _rects.Clear(); while (tiles.MoveNext(out var tileRef)) { if (!predicate(grid, tileRef)) continue; var bounds = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize); _rects.Add(new WorldRect(bounds, Color.White)); } worldHandle.DrawRectsUnmodulated(CollectionsMarshal.AsSpan(_rects)); } }, Color.Transparent); return targetRes.Target!; } public override void Shutdown() { base.Shutdown(); _resources.Dispose(); } private sealed class CachedResources : IDisposable { private readonly Dictionary _targets = new(); public CachedTarget GetOrCreate(string key) { if (_targets.TryGetValue(key, out var target)) return target; target = new CachedTarget(); _targets.Add(key, target); return target; } public void Dispose() { foreach (var target in _targets.Values) { target.Target?.Dispose(); } _targets.Clear(); } } private sealed class CachedTarget { public IRenderTexture? Target; public uint LastFrame; public MapId LastMapId; public Box2Rotated LastWorldBounds; } }