diff --git a/Resources/EnginePrototypes/Shaders/stockshaders.yml b/Resources/EnginePrototypes/Shaders/stockshaders.yml index 63e2dcf6890..13f0d6361fe 100644 --- a/Resources/EnginePrototypes/Shaders/stockshaders.yml +++ b/Resources/EnginePrototypes/Shaders/stockshaders.yml @@ -4,6 +4,12 @@ kind: canvas light_mode: unshaded +# Simple mix blend +- type: shader + id: Mix + kind: canvas + blend_mode: Mix + - type: shader id: shaded kind: canvas diff --git a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs index ea9271562a3..6fd6c4ff9ca 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs @@ -423,11 +423,18 @@ private void RenderInRenderTarget(RenderTargetBase rt, Action a, Color? clearCol var oldTransform = _currentMatrixModel; var oldScissor = _currentScissorState; + var oldMatrixProj = _currentMatrixProj; + var oldMatrixView = _currentMatrixView; + var oldBoundTarget = _currentBoundRenderTarget; + var oldRenderTarget = _currentRenderTarget; + var oldShader = _queuedShaderInstance; + + // Need to get state before flushing render queue in case they modify the original state. + var state = PushRenderStateFull(); // Have to flush the render queue so that all commands finish rendering to the previous framebuffer. FlushRenderQueue(); - var state = PushRenderStateFull(); { BindRenderTargetFull(RtToLoaded(rt)); @@ -452,6 +459,14 @@ private void RenderInRenderTarget(RenderTargetBase rt, Action a, Color? clearCol SetScissorFull(oldScissor); _currentMatrixModel = oldTransform; + + DebugTools.Assert(_currentMatrixModel.Equals(oldTransform)); + DebugTools.Assert(_currentScissorState.Equals(oldScissor)); + DebugTools.Assert(_currentMatrixProj.Equals(oldMatrixProj)); + DebugTools.Assert(oldMatrixView.Equals(_currentMatrixView)); + DebugTools.Assert(oldRenderTarget.Equals(_currentRenderTarget)); + DebugTools.Assert(oldBoundTarget.Equals(_currentBoundRenderTarget)); + DebugTools.Assert(oldShader.Equals(_queuedShaderInstance)); } private void RenderViewport(Viewport viewport) diff --git a/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs index 80008a627f3..bf8f1eb8dd7 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs @@ -16,7 +16,9 @@ using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp; using Robust.Shared.Physics; using Robust.Client.ComponentTrees; +using Robust.Shared.Enums; using Robust.Shared.Graphics; +using Robust.Shared.Prototypes; using static Robust.Shared.GameObjects.OccluderComponent; using Robust.Shared.Utility; using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode; @@ -402,6 +404,7 @@ private void DrawLightsAndFov(Viewport viewport, Box2Rotated worldBounds, Box2 w CheckGlError(); BindRenderTargetImmediate(RtToLoaded(viewport.LightRenderTarget)); + DebugTools.Assert(_currentBoundRenderTarget.TextureHandle.Equals(viewport.LightRenderTarget.Texture.TextureId)); CheckGlError(); GLClearColor(_entityManager.GetComponentOrNull(mapUid)?.AmbientLightColor ?? MapLightComponent.DefaultColor); GL.ClearStencil(0xFF); @@ -409,6 +412,35 @@ private void DrawLightsAndFov(Viewport viewport, Box2Rotated worldBounds, Box2 w GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit); CheckGlError(); + var oldTarget = _currentRenderTarget; + var oldProj = _currentMatrixProj; + var oldShader = _queuedShaderInstance; + var oldModel = _currentMatrixModel; + var oldScissor = _currentScissorState; + var oldScissoring = _isScissoring; + var state = PushRenderStateFull(); + + RenderOverlays(viewport, OverlaySpace.BeforeLighting, worldAABB, worldBounds); + + if (_lightManager.BlurFactor != 0f && viewport.Eye != null) + BlurLights(viewport, viewport.LightRenderTarget, viewport.Eye, _lightManager.BlurFactor); + + // Batching doesn't restore stencil state so we do it here. + // Yes I spent 8 hours across 2 days just to track down this as the problem. + GL.Enable(EnableCap.StencilTest); + GL.ClearStencil(0xFF); + GL.StencilMask(0xFF); + GL.Clear(ClearBufferMask.StencilBufferBit); + PopRenderStateFull(state); + _isStencilling = true; + DebugTools.Assert(oldScissoring.Equals(_isScissoring)); + DebugTools.Assert(oldScissor.Equals(_currentScissorState)); + DebugTools.Assert(oldModel.Equals(_currentMatrixModel)); + DebugTools.Assert(oldShader.Equals(_queuedShaderInstance)); + DebugTools.Assert(oldProj.Equals(_currentMatrixProj)); + DebugTools.Assert(oldTarget.Equals(_currentRenderTarget)); + DebugTools.Assert(_currentBoundRenderTarget.TextureHandle.Equals(viewport.LightRenderTarget.Texture.TextureId)); + ApplyLightingFovToBuffer(viewport, eye); var lightShader = _loadedShaders[_enableSoftShadows ? _lightSoftShaderHandle : _lightHardShaderHandle] @@ -515,7 +547,7 @@ private void DrawLightsAndFov(Viewport viewport, Box2Rotated worldBounds, Box2 w CheckGlError(); if (_cfg.GetCVar(CVars.LightBlur)) - BlurLights(viewport, eye); + BlurLights(viewport, viewport.LightRenderTarget, eye); using (_prof.Group("BlurOntoWalls")) { @@ -531,9 +563,8 @@ private void DrawLightsAndFov(Viewport viewport, Box2Rotated worldBounds, Box2 w GL.Viewport(0, 0, viewport.Size.X, viewport.Size.Y); CheckGlError(); - Array.Clear(_lightsToRenderList, 0, count); - _lightingReady = true; + Array.Clear(_lightsToRenderList, 0, count); } private static bool LightQuery(ref ( @@ -643,10 +674,14 @@ public int Compare( return (state.count, expandedBounds); } - private void BlurLights(Viewport viewport, IEye eye) + private void BlurLights(IClydeViewport viewport, IRenderTarget target, IEye eye, float multiplier = 14f) { + if (target is not RenderTexture rTexture || viewport is not Viewport rViewport) + return; + using var _ = DebugGroup(nameof(BlurLights)); + var state = PushRenderStateFull(); GL.Disable(EnableCap.Blend); CheckGlError(); CalcScreenMatrices(viewport.Size, out var proj, out var view); @@ -655,9 +690,9 @@ private void BlurLights(Viewport viewport, IEye eye) var shader = _loadedShaders[_lightBlurShaderHandle].Program; shader.Use(); - SetupGlobalUniformsImmediate(shader, viewport.LightRenderTarget.Texture); + SetupGlobalUniformsImmediate(shader, rTexture.Texture); - var size = viewport.LightRenderTarget.Size; + var size = target.Size; shader.SetUniformMaybe("size", (Vector2)size); shader.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0); @@ -667,14 +702,13 @@ private void BlurLights(Viewport viewport, IEye eye) // Initially we're pulling from the light render target. // So we set it out of the loop so // _wallBleedIntermediateRenderTarget2 gets bound at the end of the loop body. - SetTexture(TextureUnit.Texture0, viewport.LightRenderTarget.Texture); + SetTexture(TextureUnit.Texture0, rTexture.Texture); // Have to scale the blurring radius based on viewport size and camera zoom. - const float refCameraHeight = 14; var facBase = _cfg.GetCVar(CVars.LightBlurFactor); var cameraSize = eye.Zoom.Y * viewport.Size.Y * (1 / viewport.RenderScale.Y) / EyeManager.PixelsPerMeter; // 7e-3f is just a magic factor that makes it look ok. - var factor = facBase * (refCameraHeight / cameraSize); + var factor = facBase * (multiplier / cameraSize); // Multi-iteration gaussian blur. for (var i = 3; i > 0; i--) @@ -683,23 +717,24 @@ private void BlurLights(Viewport viewport, IEye eye) // Set factor. shader.SetUniformMaybe("radius", scale); - BindRenderTargetFull(viewport.LightBlurTarget); + BindRenderTargetImmediate(RtToLoaded(rViewport.LightBlurTarget)); // Blur horizontally to _wallBleedIntermediateRenderTarget1. shader.SetUniformMaybe("direction", Vector2.UnitX); _drawQuad(Vector2.Zero, viewport.Size, Matrix3x2.Identity, shader); - SetTexture(TextureUnit.Texture0, viewport.LightBlurTarget.Texture); + SetTexture(TextureUnit.Texture0, rViewport.LightBlurTarget.Texture); - BindRenderTargetFull(viewport.LightRenderTarget); + BindRenderTargetImmediate(RtToLoaded(rTexture)); // Blur vertically to _wallBleedIntermediateRenderTarget2. shader.SetUniformMaybe("direction", Vector2.UnitY); _drawQuad(Vector2.Zero, viewport.Size, Matrix3x2.Identity, shader); - SetTexture(TextureUnit.Texture0, viewport.LightRenderTarget.Texture); + SetTexture(TextureUnit.Texture0, rTexture.Texture); } + PopRenderStateFull(state); GL.Enable(EnableCap.Blend); CheckGlError(); // We didn't trample over the old _currentMatrices so just roll it back. @@ -1135,22 +1170,20 @@ private void RegenLightRts(Viewport viewport) var lightMapSize = GetLightMapSize(viewport.Size); var lightMapSizeQuart = GetLightMapSize(viewport.Size, true); - var lightMapColorFormat = _hasGLFloatFramebuffers - ? RenderTargetColorFormat.R11FG11FB10F - : RenderTargetColorFormat.Rgba8; - var lightMapSampleParameters = new TextureSampleParameters { Filter = true }; viewport.LightRenderTarget?.Dispose(); viewport.WallMaskRenderTarget?.Dispose(); viewport.WallBleedIntermediateRenderTarget1?.Dispose(); viewport.WallBleedIntermediateRenderTarget2?.Dispose(); + var lightMapColorFormat = _hasGLFloatFramebuffers + ? RenderTargetColorFormat.R11FG11FB10F + : RenderTargetColorFormat.Rgba8; + var lightMapSampleParameters = new TextureSampleParameters { Filter = true }; viewport.WallMaskRenderTarget = CreateRenderTarget(viewport.Size, RenderTargetColorFormat.R8, name: $"{viewport.Name}-{nameof(viewport.WallMaskRenderTarget)}"); - viewport.LightRenderTarget = CreateRenderTarget(lightMapSize, - new RenderTargetFormatParameters(lightMapColorFormat, hasDepthStencil: true), - lightMapSampleParameters, + viewport.LightRenderTarget = (RenderTexture) CreateLightRenderTarget(lightMapSize, $"{viewport.Name}-{nameof(viewport.LightRenderTarget)}"); viewport.LightBlurTarget = CreateRenderTarget(lightMapSize, @@ -1158,11 +1191,13 @@ private void RegenLightRts(Viewport viewport) lightMapSampleParameters, $"{viewport.Name}-{nameof(viewport.LightBlurTarget)}"); - viewport.WallBleedIntermediateRenderTarget1 = CreateRenderTarget(lightMapSizeQuart, lightMapColorFormat, + viewport.WallBleedIntermediateRenderTarget1 = CreateRenderTarget(lightMapSizeQuart, + new RenderTargetFormatParameters(lightMapColorFormat), lightMapSampleParameters, $"{viewport.Name}-{nameof(viewport.WallBleedIntermediateRenderTarget1)}"); - viewport.WallBleedIntermediateRenderTarget2 = CreateRenderTarget(lightMapSizeQuart, lightMapColorFormat, + viewport.WallBleedIntermediateRenderTarget2 = CreateRenderTarget(lightMapSizeQuart, + new RenderTargetFormatParameters(lightMapColorFormat), lightMapSampleParameters, $"{viewport.Name}-{nameof(viewport.WallBleedIntermediateRenderTarget2)}"); } diff --git a/Robust.Client/Graphics/Clyde/Clyde.RenderTargets.cs b/Robust.Client/Graphics/Clyde/Clyde.RenderTargets.cs index a07f1cfc7ed..5ea57a1aeac 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.RenderTargets.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.RenderTargets.cs @@ -30,6 +30,20 @@ private readonly ConcurrentQueue _renderTargetDisposeQueue // It, like _mainWindowRenderTarget, is initialized in Clyde's constructor private LoadedRenderTarget _currentBoundRenderTarget; + + public IRenderTexture CreateLightRenderTarget(Vector2i size, string? name = null, bool depthStencil = true) + { + var lightMapColorFormat = _hasGLFloatFramebuffers + ? RTCF.R11FG11FB10F + : RTCF.Rgba8; + var lightMapSampleParameters = new TextureSampleParameters { Filter = true }; + + return CreateRenderTarget(size, + new RenderTargetFormatParameters(lightMapColorFormat, hasDepthStencil: depthStencil), + lightMapSampleParameters, + name: name); + } + IRenderTexture IClyde.CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format, TextureSampleParameters? sampleParameters, string? name) { @@ -204,7 +218,8 @@ private RenderTexture CreateRenderTarget(Vector2i size, RenderTargetFormatParame Size = size, TextureHandle = textureObject.TextureId, MemoryPressure = pressure, - ColorFormat = format.ColorFormat + ColorFormat = format.ColorFormat, + SampleParameters = sampleParameters, }; //GC.AddMemoryPressure(pressure); @@ -251,9 +266,15 @@ private void BindRenderTargetFull(RenderTargetBase rt) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private LoadedRenderTarget RtToLoaded(RenderTargetBase rt) + private LoadedRenderTarget RtToLoaded(IRenderTarget rt) { - return _renderTargets[rt.Handle]; + switch (rt) + { + case RenderTargetBase based: + return _renderTargets[based.Handle]; + default: + throw new NotImplementedException(); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -302,6 +323,8 @@ private sealed class LoadedRenderTarget // Renderbuffer handle public GLHandle DepthStencilHandle; public long MemoryPressure; + + public TextureSampleParameters? SampleParameters; } private abstract class RenderTargetBase : IRenderTarget diff --git a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs index af19a2c1a2e..3da618f01f1 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs @@ -859,15 +859,17 @@ private void BreakBatch() private FullStoredRendererState PushRenderStateFull() { - return new FullStoredRendererState(_currentMatrixProj, _currentMatrixView, _currentRenderTarget); + return new FullStoredRendererState(_currentMatrixProj, _currentMatrixView, _currentBoundRenderTarget, _currentRenderTarget, _queuedShaderInstance); } private void PopRenderStateFull(in FullStoredRendererState state) { SetProjViewFull(state.ProjMatrix, state.ViewMatrix); - BindRenderTargetFull(state.RenderTarget); + BindRenderTargetImmediate(state.BoundRenderTarget); - var (width, height) = state.RenderTarget.Size; + _queuedShaderInstance = state.QueuedShaderInstance; + _currentRenderTarget = state.RenderTarget; + var (width, height) = state.BoundRenderTarget.Size; GL.Viewport(0, 0, width, height); CheckGlError(); } @@ -1061,14 +1063,23 @@ private readonly struct FullStoredRendererState { public readonly Matrix3x2 ProjMatrix; public readonly Matrix3x2 ViewMatrix; + public readonly LoadedRenderTarget BoundRenderTarget; public readonly LoadedRenderTarget RenderTarget; - - public FullStoredRendererState(in Matrix3x2 projMatrix, in Matrix3x2 viewMatrix, - LoadedRenderTarget renderTarget) + public readonly ClydeShaderInstance QueuedShaderInstance; + + public FullStoredRendererState( + in Matrix3x2 projMatrix, + in Matrix3x2 viewMatrix, + LoadedRenderTarget boundRenderTarget, + LoadedRenderTarget renderTarget, + ClydeShaderInstance queuedShaderInstance + ) { ProjMatrix = projMatrix; ViewMatrix = viewMatrix; + BoundRenderTarget = boundRenderTarget; RenderTarget = renderTarget; + QueuedShaderInstance = queuedShaderInstance; } } } diff --git a/Robust.Client/Graphics/Clyde/Clyde.cs b/Robust.Client/Graphics/Clyde/Clyde.cs index 736116e5abc..85ceb609726 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.cs @@ -21,6 +21,7 @@ using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Profiling; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode; @@ -36,6 +37,7 @@ internal sealed partial class Clyde : IClydeInternal, IPostInjectInit, IEntityEv [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IOverlayManager _overlayManager = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; [Dependency] private readonly IResourceCache _resourceCache = default!; [Dependency] private readonly IResourceManager _resManager = default!; [Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!; diff --git a/Robust.Client/Graphics/Clyde/ClydeHeadless.cs b/Robust.Client/Graphics/Clyde/ClydeHeadless.cs index 5754702641c..f56a6b1d161 100644 --- a/Robust.Client/Graphics/Clyde/ClydeHeadless.cs +++ b/Robust.Client/Graphics/Clyde/ClydeHeadless.cs @@ -188,6 +188,11 @@ public OwnedTexture CreateBlankTexture( return new DummyTexture(size); } + public IRenderTexture CreateLightRenderTarget(Vector2i size, string? name = null, bool depthStencil = true) + { + return CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.R8, hasDepthStencil: depthStencil), null, name: name); + } + public IRenderTexture CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format, TextureSampleParameters? sampleParameters = null, string? name = null) { diff --git a/Robust.Client/Graphics/IClyde.cs b/Robust.Client/Graphics/IClyde.cs index 348e705e943..55d4fd1b227 100644 --- a/Robust.Client/Graphics/IClyde.cs +++ b/Robust.Client/Graphics/IClyde.cs @@ -71,6 +71,8 @@ OwnedTexture CreateBlankTexture( in TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel; + IRenderTexture CreateLightRenderTarget(Vector2i size, string? name = null, bool depthStencil = true); + IRenderTexture CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format, TextureSampleParameters? sampleParameters = null, string? name = null); diff --git a/Robust.Client/Graphics/IRenderTarget.cs b/Robust.Client/Graphics/IRenderTarget.cs index 61b27762967..dc8cc53c5dd 100644 --- a/Robust.Client/Graphics/IRenderTarget.cs +++ b/Robust.Client/Graphics/IRenderTarget.cs @@ -1,4 +1,6 @@ using System; +using System.Numerics; +using Robust.Shared.Graphics; using Robust.Shared.Maths; using SixLabors.ImageSharp.PixelFormats; @@ -15,5 +17,42 @@ public interface IRenderTarget : IDisposable Vector2i Size { get; } void CopyPixelsToMemory(CopyPixelsDelegate callback, UIBox2i? subRegion = null) where T : unmanaged, IPixel; + + public Vector2 LocalToWorld(IEye eye, Vector2 point, Vector2 scale) + { + var newPoint = point; + + // (inlined version of UiProjMatrix^-1) + newPoint -= Size / 2f; + newPoint *= new Vector2(1, -1) / EyeManager.PixelsPerMeter; + + // view matrix + eye.GetViewMatrixInv(out var viewMatrixInv, scale); + newPoint = Vector2.Transform(newPoint, viewMatrixInv); + + return newPoint; + } + + public Vector2 WorldToLocal(Vector2 point, IEye eye, Vector2 scale) + { + var newPoint = point; + + eye.GetViewMatrix(out var viewMatrix, scale); + newPoint = Vector2.Transform(newPoint, viewMatrix); + + // (inlined version of UiProjMatrix) + newPoint *= new Vector2(1, -1) * EyeManager.PixelsPerMeter; + newPoint += Size / 2f; + + return newPoint; + } + + public Matrix3x2 GetWorldToLocalMatrix(IEye eye, Vector2 scale) + { + eye.GetViewMatrix(out var viewMatrix, scale * new Vector2(EyeManager.PixelsPerMeter, -EyeManager.PixelsPerMeter)); + viewMatrix.M31 += Size.X / 2f; + viewMatrix.M32 += Size.Y / 2f; + return viewMatrix; + } } } diff --git a/Robust.Client/Graphics/Lighting/ILightManager.cs b/Robust.Client/Graphics/Lighting/ILightManager.cs index 3204c4689cd..7e6e07f4a05 100644 --- a/Robust.Client/Graphics/Lighting/ILightManager.cs +++ b/Robust.Client/Graphics/Lighting/ILightManager.cs @@ -24,5 +24,10 @@ public interface ILightManager /// This is useful to prevent players messing with lighting setup when they shouldn't. /// bool LockConsoleAccess { get; set; } + + /// + /// How much to blur the lighting render target by after BeforeLighting overlays have run. + /// + float BlurFactor { get; set; } } } diff --git a/Robust.Client/Graphics/Lighting/LightManager.cs b/Robust.Client/Graphics/Lighting/LightManager.cs index 19bb4c4986b..5399cd54a09 100644 --- a/Robust.Client/Graphics/Lighting/LightManager.cs +++ b/Robust.Client/Graphics/Lighting/LightManager.cs @@ -9,6 +9,9 @@ public sealed class LightManager : ILightManager public bool DrawHardFov { get; set; } = true; public bool DrawLighting { get; set; } = true; public bool LockConsoleAccess { get; set; } = false; + public Color AmbientLightColor { get; set; } = Color.FromSrgb(Color.Black); + + public float BlurFactor { get; set; } = 14f * 5f; } } diff --git a/Robust.Shared/Enums/OverlaySpaces.cs b/Robust.Shared/Enums/OverlaySpaces.cs index deefdc64c71..28be50a7673 100644 --- a/Robust.Shared/Enums/OverlaySpaces.cs +++ b/Robust.Shared/Enums/OverlaySpaces.cs @@ -54,6 +54,11 @@ public enum OverlaySpace : ushort /// /// Overlay will be rendered below grids, entities, and everything else. In world space. /// - WorldSpaceBelowWorld = 1 << 8 + WorldSpaceBelowWorld = 1 << 8, + + /// + /// Called after GLClear but before FOV applied to the lighting buffer. + /// + BeforeLighting = 1 << 9, } } diff --git a/Robust.Shared/Map/Tile.cs b/Robust.Shared/Map/Tile.cs index 23c6ad9f389..35304d7225e 100644 --- a/Robust.Shared/Map/Tile.cs +++ b/Robust.Shared/Map/Tile.cs @@ -1,5 +1,6 @@ using System; using JetBrains.Annotations; +using Robust.Shared.Serialization; using Robust.Shared.Utility; namespace Robust.Shared.Map; @@ -20,6 +21,11 @@ namespace Robust.Shared.Map; /// public readonly TileRenderFlag Flags; + /// + /// Tile-flags for content usage. + /// + public readonly ushort ContentFlag; + /// /// Variant of this tile to render. /// @@ -41,11 +47,13 @@ namespace Robust.Shared.Map; /// Internal type ID. /// Flags used by toolbox's rendering. /// The visual variant this tile is using. - public Tile(int typeId, TileRenderFlag flags = 0, byte variant = 0) + /// + public Tile(int typeId, TileRenderFlag flags = 0, byte variant = 0, ushort contentFlag = 0) { TypeId = typeId; Flags = flags; Variant = variant; + ContentFlag = contentFlag; } /// @@ -93,7 +101,7 @@ public bool TryFormat( /// public bool Equals(Tile other) { - return TypeId == other.TypeId && Flags == other.Flags && Variant == other.Variant; + return TypeId == other.TypeId && Flags == other.Flags && Variant == other.Variant && ContentFlag == other.ContentFlag; } /// @@ -112,8 +120,22 @@ public override int GetHashCode() return (TypeId.GetHashCode() * 397) ^ Flags.GetHashCode() ^ Variant.GetHashCode(); } } + + [Pure] + public Tile WithContentFlag(ushort tileContentFlag) + { + return new Tile(typeId: TypeId, flags: Flags, variant: Variant, contentFlag: tileContentFlag); + } + + [Pure] + public Tile WithVariant(byte variant) + { + return new Tile(TypeId, Flags, variant, ContentFlag); + } } +public sealed class TileFlagLayer {} + public enum TileRenderFlag : byte {