]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add planet lighting (#32522)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Sun, 16 Feb 2025 08:35:32 +0000 (19:35 +1100)
committerGitHub <noreply@github.com>
Sun, 16 Feb 2025 08:35:32 +0000 (19:35 +1100)
* Implements a Dynamic Lighting System on maps.

* Edit: the night should be a little bit brighter and blue now.

* Major edit: everything must be done on the client side now, with certain datafield replicated.
Changes were outlined in the salvage to accommodate the new lighting system.

* Edit: The offset is now serverside, this makes the time accurate in all situations.

* Removing ununsed import

* Minor tweaks

* Tweak in time precision

* Minor tweak + Unused import removed

* Edit: apparently RealTime is better for what I'm looking for

* Fix: Now the time is calculated correctly.

* Minor tweaks

* Adds condition for when the light should be updated

* Add planet lighting

* she

* close-ish

* c

* bittersweat

* Fixes

* Revert "Merge branch '22719' into 2024-09-29-planet-lighting"

This reverts commit 9f2785bb16aee47d794aa3eed8ae15004f97fc35, reversing
changes made to 19649c07a5fb625423e08fc18d91c9cb101daa86.

* Europa and day-night

* weh

* rooves working

* Clean

* Remove Europa

* Fixes

* fix

* Update

* Fix caves

* Update for engine

* Add sun shadows (planet lighting v2)

For now mostly targeting walls and having the shadows change over time. Got the basic proof-of-concept working just needs a hell of a lot of polish.

* Documentation

* a

* Fixes

* Move blur to an overlay

* Slughands

* Fixes

* Remove v2 work

* Finalise

---------

Co-authored-by: DoutorWhite <thedoctorwhite@gmail.com>
26 files changed:
Content.Client/Light/AfterLightTargetOverlay.cs [new file with mode: 0644]
Content.Client/Light/BeforeLightTargetOverlay.cs [new file with mode: 0644]
Content.Client/Light/EntitySystems/PlanetLightSystem.cs [new file with mode: 0644]
Content.Client/Light/EntitySystems/RoofSystem.cs [new file with mode: 0644]
Content.Client/Light/LightBlurOverlay.cs [new file with mode: 0644]
Content.Client/Light/LightCycleSystem.cs [new file with mode: 0644]
Content.Client/Light/RoofOverlay.cs [new file with mode: 0644]
Content.Client/Light/TileEmissionOverlay.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/SaveLoadMapTest.cs
Content.Server/Light/Components/SetRoofComponent.cs [new file with mode: 0644]
Content.Server/Light/EntitySystems/LightCycleSystem.cs [new file with mode: 0644]
Content.Server/Light/EntitySystems/RoofSystem.cs [new file with mode: 0644]
Content.Server/Parallax/BiomeSystem.cs
Content.Shared/Light/Components/LightCycleComponent.cs [new file with mode: 0644]
Content.Shared/Light/Components/RoofComponent.cs [new file with mode: 0644]
Content.Shared/Light/Components/TileEmissionComponent.cs [new file with mode: 0644]
Content.Shared/Light/EntitySystems/SharedRoofSystem.cs [new file with mode: 0644]
Content.Shared/Maps/ContentTileDefinition.cs
Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs
Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs
Content.Shared/SharedLightCycleSystem.cs [new file with mode: 0644]
Resources/Prototypes/Entities/Markers/tile.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Structures/Walls/walls.yml
Resources/Prototypes/Entities/Tiles/lava.yml
Resources/Prototypes/Entities/Tiles/liquid_plasma.yml
Resources/Prototypes/Procedural/biome_templates.yml

diff --git a/Content.Client/Light/AfterLightTargetOverlay.cs b/Content.Client/Light/AfterLightTargetOverlay.cs
new file mode 100644 (file)
index 0000000..5cfe1e9
--- /dev/null
@@ -0,0 +1,58 @@
+using System.Numerics;
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+
+namespace Content.Client.Light;
+
+/// <summary>
+/// This exists just to copy <see cref="BeforeLightTargetOverlay"/> to the light render target
+/// </summary>
+public sealed class AfterLightTargetOverlay : Overlay
+{
+    public override OverlaySpace Space => OverlaySpace.BeforeLighting;
+
+    [Dependency] private readonly IOverlayManager _overlay = default!;
+
+    public const int ContentZIndex = LightBlurOverlay.ContentZIndex + 1;
+
+    public AfterLightTargetOverlay()
+    {
+        IoCManager.InjectDependencies(this);
+        ZIndex = ContentZIndex;
+    }
+
+    protected override void Draw(in OverlayDrawArgs args)
+    {
+        var viewport = args.Viewport;
+        var worldHandle = args.WorldHandle;
+
+        if (viewport.Eye == null)
+            return;
+
+        var lightOverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
+        var bounds = args.WorldBounds;
+
+        // at 1-1 render scale it's mostly fine but at 4x4 it's way too fkn big
+        var newScale = viewport.RenderScale / 2f;
+
+        var localMatrix =
+            viewport.LightRenderTarget.GetWorldToLocalMatrix(viewport.Eye, newScale);
+        var diff = (lightOverlay.EnlargedLightTarget.Size - viewport.LightRenderTarget.Size);
+        var halfDiff = diff / 2;
+
+        // Pixels -> Metres -> Half distance.
+        // If we're zoomed in need to enlarge the bounds further.
+        args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
+            () =>
+            {
+                // We essentially need to draw the cropped version onto the lightrendertarget.
+                var subRegion = new UIBox2i(halfDiff.X,
+                    halfDiff.Y,
+                    viewport.LightRenderTarget.Size.X + halfDiff.X,
+                    viewport.LightRenderTarget.Size.Y + halfDiff.Y);
+
+                worldHandle.SetTransform(localMatrix);
+                worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
+            }, null);
+    }
+}
diff --git a/Content.Client/Light/BeforeLightTargetOverlay.cs b/Content.Client/Light/BeforeLightTargetOverlay.cs
new file mode 100644 (file)
index 0000000..add172c
--- /dev/null
@@ -0,0 +1,51 @@
+using System.Numerics;
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+
+namespace Content.Client.Light;
+
+/// <summary>
+/// Handles an enlarged lighting target so content can use large blur radii.
+/// </summary>
+public sealed class BeforeLightTargetOverlay : Overlay
+{
+    public override OverlaySpace Space => OverlaySpace.BeforeLighting;
+
+    [Dependency] private readonly IClyde _clyde = default!;
+
+    public IRenderTexture EnlargedLightTarget = default!;
+    public Box2Rotated EnlargedBounds;
+
+    /// <summary>
+    /// In metres
+    /// </summary>
+    private float _skirting = 1.5f;
+
+    public const int ContentZIndex = -10;
+
+    public BeforeLightTargetOverlay()
+    {
+        IoCManager.InjectDependencies(this);
+        ZIndex = ContentZIndex;
+    }
+
+    protected override void Draw(in OverlayDrawArgs args)
+    {
+        // Code is weird but I don't think engine should be enlarging the lighting render target arbitrarily either, maybe via cvar?
+        // The problem is the blur has no knowledge of pixels outside the viewport so with a large enough blur radius you get sampling issues.
+        var size = args.Viewport.LightRenderTarget.Size + (int) (_skirting * EyeManager.PixelsPerMeter);
+        EnlargedBounds = args.WorldBounds.Enlarged(_skirting / 2f);
+
+        // This just exists to copy the lightrendertarget and write back to it.
+        if (EnlargedLightTarget?.Size != size)
+        {
+            EnlargedLightTarget = _clyde
+                .CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-copy");
+        }
+
+        args.WorldHandle.RenderInRenderTarget(EnlargedLightTarget,
+            () =>
+            {
+            }, _clyde.GetClearColor(args.MapUid));
+    }
+}
diff --git a/Content.Client/Light/EntitySystems/PlanetLightSystem.cs b/Content.Client/Light/EntitySystems/PlanetLightSystem.cs
new file mode 100644 (file)
index 0000000..2da6713
--- /dev/null
@@ -0,0 +1,36 @@
+using Robust.Client.Graphics;
+
+namespace Content.Client.Light.EntitySystems;
+
+public sealed class PlanetLightSystem : EntitySystem
+{
+    [Dependency] private readonly IOverlayManager _overlayMan = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<GetClearColorEvent>(OnClearColor);
+
+        _overlayMan.AddOverlay(new BeforeLightTargetOverlay());
+        _overlayMan.AddOverlay(new RoofOverlay(EntityManager));
+        _overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));
+        _overlayMan.AddOverlay(new LightBlurOverlay());
+        _overlayMan.AddOverlay(new AfterLightTargetOverlay());
+    }
+
+    private void OnClearColor(ref GetClearColorEvent ev)
+    {
+        ev.Color = Color.Transparent;
+    }
+
+    public override void Shutdown()
+    {
+        base.Shutdown();
+        _overlayMan.RemoveOverlay<BeforeLightTargetOverlay>();
+        _overlayMan.RemoveOverlay<RoofOverlay>();
+        _overlayMan.RemoveOverlay<TileEmissionOverlay>();
+        _overlayMan.RemoveOverlay<LightBlurOverlay>();
+        _overlayMan.RemoveOverlay<AfterLightTargetOverlay>();
+    }
+}
diff --git a/Content.Client/Light/EntitySystems/RoofSystem.cs b/Content.Client/Light/EntitySystems/RoofSystem.cs
new file mode 100644 (file)
index 0000000..559eea4
--- /dev/null
@@ -0,0 +1,9 @@
+using Content.Shared.Light.EntitySystems;
+
+namespace Content.Client.Light.EntitySystems;
+
+/// <inheritdoc/>
+public sealed class RoofSystem : SharedRoofSystem
+{
+
+}
diff --git a/Content.Client/Light/LightBlurOverlay.cs b/Content.Client/Light/LightBlurOverlay.cs
new file mode 100644 (file)
index 0000000..ae0684f
--- /dev/null
@@ -0,0 +1,44 @@
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+
+namespace Content.Client.Light;
+
+/// <summary>
+/// Essentially handles blurring for content-side light overlays.
+/// </summary>
+public sealed class LightBlurOverlay : Overlay
+{
+    public override OverlaySpace Space => OverlaySpace.BeforeLighting;
+
+    [Dependency] private readonly IClyde _clyde = default!;
+    [Dependency] private readonly IOverlayManager _overlay = default!;
+
+    public const int ContentZIndex = TileEmissionOverlay.ContentZIndex + 1;
+
+    private IRenderTarget? _blurTarget;
+
+    public LightBlurOverlay()
+    {
+        IoCManager.InjectDependencies(this);
+        ZIndex = ContentZIndex;
+    }
+
+    protected override void Draw(in OverlayDrawArgs args)
+    {
+        if (args.Viewport.Eye == null)
+            return;
+
+        var beforeOverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
+        var size = beforeOverlay.EnlargedLightTarget.Size;
+
+        if (_blurTarget?.Size != size)
+        {
+            _blurTarget = _clyde
+                .CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-blur");
+        }
+
+        var target = beforeOverlay.EnlargedLightTarget;
+        // Yeah that's all this does keep walkin.
+        _clyde.BlurRenderTarget(args.Viewport, target, _blurTarget, args.Viewport.Eye, 14f * 2f);
+    }
+}
diff --git a/Content.Client/Light/LightCycleSystem.cs b/Content.Client/Light/LightCycleSystem.cs
new file mode 100644 (file)
index 0000000..9e19423
--- /dev/null
@@ -0,0 +1,33 @@
+using Content.Client.GameTicking.Managers;
+using Content.Shared;
+using Content.Shared.Light.Components;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Light;
+
+/// <inheritdoc/>
+public sealed class LightCycleSystem : SharedLightCycleSystem
+{
+    [Dependency] private readonly ClientGameTicker _ticker = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+        var mapQuery = AllEntityQuery<LightCycleComponent, MapLightComponent>();
+        while (mapQuery.MoveNext(out var uid,  out var cycle, out var map))
+        {
+            if (!cycle.Running)
+                continue;
+
+            var time = (float) _timing.CurTime
+                .Add(cycle.Offset)
+                .Subtract(_ticker.RoundStartTimeSpan)
+                .TotalSeconds;
+
+            var color = GetColor((uid, cycle), cycle.OriginalColor, time);
+            map.AmbientLightColor = color;
+        }
+    }
+}
diff --git a/Content.Client/Light/RoofOverlay.cs b/Content.Client/Light/RoofOverlay.cs
new file mode 100644 (file)
index 0000000..981edf7
--- /dev/null
@@ -0,0 +1,100 @@
+using System.Numerics;
+using Content.Shared.Light.Components;
+using Content.Shared.Maps;
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+using Robust.Shared.Map.Components;
+
+namespace Content.Client.Light;
+
+public sealed class RoofOverlay : Overlay
+{
+    private readonly IEntityManager _entManager;
+    [Dependency] private readonly IOverlayManager _overlay = default!;
+
+    private readonly EntityLookupSystem _lookup;
+    private readonly SharedMapSystem _mapSystem;
+    private readonly SharedTransformSystem _xformSystem;
+
+    private readonly HashSet<Entity<OccluderComponent>> _occluders = new();
+
+    public override OverlaySpace Space => OverlaySpace.BeforeLighting;
+
+    public const int ContentZIndex = BeforeLightTargetOverlay.ContentZIndex + 1;
+
+    public RoofOverlay(IEntityManager entManager)
+    {
+        _entManager = entManager;
+        IoCManager.InjectDependencies(this);
+
+        _lookup = _entManager.System<EntityLookupSystem>();
+        _mapSystem = _entManager.System<SharedMapSystem>();
+        _xformSystem = _entManager.System<SharedTransformSystem>();
+
+        ZIndex = ContentZIndex;
+    }
+
+    protected override void Draw(in OverlayDrawArgs args)
+    {
+        if (args.Viewport.Eye == null)
+            return;
+
+        var mapEnt = _mapSystem.GetMap(args.MapId);
+
+        if (!_entManager.TryGetComponent(mapEnt, out RoofComponent? roofComp) ||
+            !_entManager.TryGetComponent(mapEnt, out MapGridComponent? grid))
+        {
+            return;
+        }
+
+        var viewport = args.Viewport;
+        var eye = args.Viewport.Eye;
+
+        var worldHandle = args.WorldHandle;
+        var lightoverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
+        var bounds = lightoverlay.EnlargedBounds;
+        var target = lightoverlay.EnlargedLightTarget;
+
+        worldHandle.RenderInRenderTarget(target,
+            () =>
+            {
+                var invMatrix = target.GetWorldToLocalMatrix(eye, viewport.RenderScale / 2f);
+
+                var gridMatrix = _xformSystem.GetWorldMatrix(mapEnt);
+                var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
+
+                worldHandle.SetTransform(matty);
+
+                var tileEnumerator = _mapSystem.GetTilesEnumerator(mapEnt, grid, bounds);
+
+                // Due to stencilling we essentially draw on unrooved tiles
+                while (tileEnumerator.MoveNext(out var tileRef))
+                {
+                    if ((tileRef.Tile.Flags & (byte) TileFlag.Roof) == 0x0)
+                    {
+                        // Check if the tile is occluded in which case hide it anyway.
+                        // This is to avoid lit walls bleeding over to unlit tiles.
+                        _occluders.Clear();
+                        _lookup.GetLocalEntitiesIntersecting(mapEnt, tileRef.GridIndices, _occluders);
+                        var found = false;
+
+                        foreach (var occluder in _occluders)
+                        {
+                            if (!occluder.Comp.Enabled)
+                                continue;
+
+                            found = true;
+                            break;
+                        }
+
+                        if (!found)
+                            continue;
+                    }
+
+                    var local = _lookup.GetLocalBounds(tileRef, grid.TileSize);
+                    worldHandle.DrawRect(local, roofComp.Color);
+                }
+
+            }, null);
+    }
+}
diff --git a/Content.Client/Light/TileEmissionOverlay.cs b/Content.Client/Light/TileEmissionOverlay.cs
new file mode 100644 (file)
index 0000000..bccc6fc
--- /dev/null
@@ -0,0 +1,90 @@
+using System.Numerics;
+using Content.Shared.Light.Components;
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+
+namespace Content.Client.Light;
+
+public sealed class TileEmissionOverlay : Overlay
+{
+    public override OverlaySpace Space => OverlaySpace.BeforeLighting;
+
+    [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly IOverlayManager _overlay = default!;
+
+    private SharedMapSystem _mapSystem;
+    private SharedTransformSystem _xformSystem;
+
+    private readonly EntityLookupSystem _lookup;
+
+    private readonly EntityQuery<TransformComponent> _xformQuery;
+    private readonly HashSet<Entity<TileEmissionComponent>> _entities = new();
+
+    private List<Entity<MapGridComponent>> _grids = new();
+
+    public const int ContentZIndex = RoofOverlay.ContentZIndex + 1;
+
+    public TileEmissionOverlay(IEntityManager entManager)
+    {
+        IoCManager.InjectDependencies(this);
+
+        _lookup = entManager.System<EntityLookupSystem>();
+        _mapSystem = entManager.System<SharedMapSystem>();
+        _xformSystem = entManager.System<SharedTransformSystem>();
+
+        _xformQuery = entManager.GetEntityQuery<TransformComponent>();
+        ZIndex = ContentZIndex;
+    }
+
+    protected override void Draw(in OverlayDrawArgs args)
+    {
+        if (args.Viewport.Eye == null)
+            return;
+
+        var mapId = args.MapId;
+        var worldHandle = args.WorldHandle;
+        var lightoverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
+        var bounds = lightoverlay.EnlargedBounds;
+        var target = lightoverlay.EnlargedLightTarget;
+        var viewport = args.Viewport;
+
+        args.WorldHandle.RenderInRenderTarget(target,
+        () =>
+        {
+            var invMatrix = target.GetWorldToLocalMatrix(viewport.Eye, viewport.RenderScale / 2f);
+            _grids.Clear();
+            _mapManager.FindGridsIntersecting(mapId, bounds, ref _grids, approx: true);
+
+            foreach (var grid in _grids)
+            {
+                var gridInvMatrix = _xformSystem.GetInvWorldMatrix(grid);
+                var localBounds = gridInvMatrix.TransformBox(bounds);
+                _entities.Clear();
+                _lookup.GetLocalEntitiesIntersecting(grid.Owner, localBounds, _entities);
+
+                if (_entities.Count == 0)
+                    continue;
+
+                var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
+
+                foreach (var ent in _entities)
+                {
+                    var xform = _xformQuery.Comp(ent);
+
+                    var tile = _mapSystem.LocalToTile(grid.Owner, grid, xform.Coordinates);
+                    var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
+
+                    worldHandle.SetTransform(matty);
+
+                    // Yes I am fully aware this leads to overlap. If you really want to have alpha then you'll need
+                    // to turn the squares into polys.
+                    // Additionally no shadows so if you make it too big it's going to go through a 1x wall.
+                    var local = _lookup.GetLocalBounds(tile, grid.Comp.TileSize).Enlarged(ent.Comp.Range);
+                    worldHandle.DrawRect(local, ent.Comp.Color);
+                }
+            }
+        }, null);
+    }
+}
index 213da5d786265a398fb132555c688249d075fdf0..1bfcd8ab608818ed5042cc93137a11fb991517e4 100644 (file)
@@ -39,12 +39,12 @@ namespace Content.IntegrationTests.Tests
                 {
                     var mapGrid = mapManager.CreateGridEntity(mapId);
                     xformSystem.SetWorldPosition(mapGrid, new Vector2(10, 10));
-                    mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(1, (TileRenderFlag) 1, 255));
+                    mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(typeId: 1, flags: 1, variant: 255));
                 }
                 {
                     var mapGrid = mapManager.CreateGridEntity(mapId);
                     xformSystem.SetWorldPosition(mapGrid, new Vector2(-8, -8));
-                    mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(2, (TileRenderFlag) 1, 254));
+                    mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(typeId: 2, flags: 1, variant: 254));
                 }
 
                 Assert.Multiple(() => mapLoader.SaveMap(mapId, mapPath));
@@ -73,7 +73,7 @@ namespace Content.IntegrationTests.Tests
                     Assert.Multiple(() =>
                     {
                         Assert.That(xformSystem.GetWorldPosition(gridXform), Is.EqualTo(new Vector2(10, 10)));
-                        Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(1, (TileRenderFlag) 1, 255)));
+                        Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(typeId: 1, flags: 1, variant: 255)));
                     });
                 }
                 {
@@ -87,7 +87,7 @@ namespace Content.IntegrationTests.Tests
                     Assert.Multiple(() =>
                     {
                         Assert.That(xformSystem.GetWorldPosition(gridXform), Is.EqualTo(new Vector2(-8, -8)));
-                        Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(2, (TileRenderFlag) 1, 254)));
+                        Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(typeId: 2, flags: 1, variant: 254)));
                     });
                 }
             });
diff --git a/Content.Server/Light/Components/SetRoofComponent.cs b/Content.Server/Light/Components/SetRoofComponent.cs
new file mode 100644 (file)
index 0000000..6bfe64a
--- /dev/null
@@ -0,0 +1,11 @@
+namespace Content.Server.Light.Components;
+
+/// <summary>
+/// Applies the roof flag to this tile and deletes the entity.
+/// </summary>
+[RegisterComponent]
+public sealed partial class SetRoofComponent : Component
+{
+    [DataField(required: true)]
+    public bool Value;
+}
diff --git a/Content.Server/Light/EntitySystems/LightCycleSystem.cs b/Content.Server/Light/EntitySystems/LightCycleSystem.cs
new file mode 100644 (file)
index 0000000..7d2eacc
--- /dev/null
@@ -0,0 +1,22 @@
+using Content.Shared;
+using Content.Shared.Light.Components;
+using Robust.Shared.Random;
+
+namespace Content.Server.Light.EntitySystems;
+
+/// <inheritdoc/>
+public sealed class LightCycleSystem : SharedLightCycleSystem
+{
+    [Dependency] private readonly IRobustRandom _random = default!;
+
+    protected override void OnCycleMapInit(Entity<LightCycleComponent> ent, ref MapInitEvent args)
+    {
+        base.OnCycleMapInit(ent, ref args);
+
+        if (ent.Comp.InitialOffset)
+        {
+            ent.Comp.Offset = _random.Next(ent.Comp.Duration);
+            Dirty(ent);
+        }
+    }
+}
diff --git a/Content.Server/Light/EntitySystems/RoofSystem.cs b/Content.Server/Light/EntitySystems/RoofSystem.cs
new file mode 100644 (file)
index 0000000..a3b8cb1
--- /dev/null
@@ -0,0 +1,33 @@
+using Content.Server.Light.Components;
+using Content.Shared.Light.EntitySystems;
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Light.EntitySystems;
+
+/// <inheritdoc/>
+public sealed class RoofSystem : SharedRoofSystem
+{
+    [Dependency] private readonly SharedMapSystem _maps = default!;
+
+    private EntityQuery<MapGridComponent> _gridQuery;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        _gridQuery = GetEntityQuery<MapGridComponent>();
+        SubscribeLocalEvent<SetRoofComponent, ComponentStartup>(OnFlagStartup);
+    }
+
+    private void OnFlagStartup(Entity<SetRoofComponent> ent, ref ComponentStartup args)
+    {
+        var xform = Transform(ent.Owner);
+
+        if (_gridQuery.TryComp(xform.GridUid, out var grid))
+        {
+            var index = _maps.LocalToTile(xform.GridUid.Value, grid, xform.Coordinates);
+            SetRoof((xform.GridUid.Value, grid, null), index, ent.Comp.Value);
+        }
+
+        QueueDel(ent.Owner);
+    }
+}
index 109aa0f6e4704e0baf8c454d5bc69a10687b953c..e419e90fd38d783bb39b6b20aab783605d71e0e9 100644 (file)
@@ -12,6 +12,7 @@ using Content.Shared.Atmos;
 using Content.Shared.Decals;
 using Content.Shared.Ghost;
 using Content.Shared.Gravity;
+using Content.Shared.Light.Components;
 using Content.Shared.Parallax.Biomes;
 using Content.Shared.Parallax.Biomes.Layers;
 using Content.Shared.Parallax.Biomes.Markers;
@@ -330,6 +331,9 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
 
         while (biomes.MoveNext(out var biome))
         {
+            if (biome.LifeStage < ComponentLifeStage.Running)
+                continue;
+
             _activeChunks.Add(biome, _tilePool.Get());
             _markerChunks.GetOrNew(biome);
         }
@@ -379,6 +383,10 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
 
         while (loadBiomes.MoveNext(out var gridUid, out var biome, out var grid))
         {
+            // If not MapInit don't run it.
+            if (biome.LifeStage < ComponentLifeStage.Running)
+                continue;
+
             if (!biome.Enabled)
                 continue;
 
@@ -745,7 +753,10 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
         }
 
         if (modified.Count == 0)
+        {
+            component.ModifiedTiles.Remove(chunk);
             _tilePool.Return(modified);
+        }
 
         component.PendingMarkers.Remove(chunk);
     }
@@ -1014,11 +1025,14 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
         // Midday: #E6CB8B
         // Moonlight: #2b3143
         // Lava: #A34931
-
         var light = EnsureComp<MapLightComponent>(mapUid);
         light.AmbientLightColor = mapLight ?? Color.FromHex("#D8B059");
         Dirty(mapUid, light, metadata);
 
+        EnsureComp<RoofComponent>(mapUid);
+
+        EnsureComp<LightCycleComponent>(mapUid);
+
         var moles = new float[Atmospherics.AdjustedNumberOfGases];
         moles[(int) Gas.Oxygen] = 21.824779f;
         moles[(int) Gas.Nitrogen] = 82.10312f;
diff --git a/Content.Shared/Light/Components/LightCycleComponent.cs b/Content.Shared/Light/Components/LightCycleComponent.cs
new file mode 100644 (file)
index 0000000..a6ce63b
--- /dev/null
@@ -0,0 +1,56 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Map.Components;
+
+namespace Content.Shared.Light.Components;
+
+/// <summary>
+/// Cycles through colors AKA "Day / Night cycle" on <see cref="MapLightComponent"/>
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class LightCycleComponent : Component
+{
+    [DataField, AutoNetworkedField]
+    public Color OriginalColor = Color.Transparent;
+
+    /// <summary>
+    /// How long an entire cycle lasts
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public TimeSpan Duration = TimeSpan.FromMinutes(30);
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan Offset;
+
+    [DataField, AutoNetworkedField]
+    public bool Enabled = true;
+
+    /// <summary>
+    /// Should the offset be randomised upon MapInit.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool InitialOffset = true;
+
+    /// <summary>
+    /// Trench of the oscillation.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float MinLightLevel = 0f;
+
+    /// <summary>
+    /// Peak of the oscillation
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float MaxLightLevel = 3f;
+
+    [DataField, AutoNetworkedField]
+    public float ClipLight = 1.25f;
+
+    [DataField, AutoNetworkedField]
+    public Color ClipLevel = new Color(1f, 1f, 1.25f);
+
+    [DataField, AutoNetworkedField]
+    public Color MinLevel = new Color(0.1f, 0.15f, 0.50f);
+
+    [DataField, AutoNetworkedField]
+    public Color MaxLevel = new Color(2f, 2f, 5f);
+}
diff --git a/Content.Shared/Light/Components/RoofComponent.cs b/Content.Shared/Light/Components/RoofComponent.cs
new file mode 100644 (file)
index 0000000..0e2adf5
--- /dev/null
@@ -0,0 +1,13 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Light.Components;
+
+/// <summary>
+/// Will draw shadows over tiles flagged as roof tiles on the attached map.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class RoofComponent : Component
+{
+    [DataField, AutoNetworkedField]
+    public Color Color = Color.Black;
+}
diff --git a/Content.Shared/Light/Components/TileEmissionComponent.cs b/Content.Shared/Light/Components/TileEmissionComponent.cs
new file mode 100644 (file)
index 0000000..0eec197
--- /dev/null
@@ -0,0 +1,16 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Light.Components;
+
+/// <summary>
+/// Will draw lighting in a range around the tile.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class TileEmissionComponent : Component
+{
+    [DataField, AutoNetworkedField]
+    public float Range = 0.25f;
+
+    [DataField(required: true), AutoNetworkedField]
+    public Color Color = Color.Transparent;
+}
diff --git a/Content.Shared/Light/EntitySystems/SharedRoofSystem.cs b/Content.Shared/Light/EntitySystems/SharedRoofSystem.cs
new file mode 100644 (file)
index 0000000..d06b5bc
--- /dev/null
@@ -0,0 +1,42 @@
+using Content.Shared.Light.Components;
+using Content.Shared.Maps;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+
+namespace Content.Shared.Light.EntitySystems;
+
+/// <summary>
+/// Handles the roof flag for tiles that gets used for the RoofOverlay.
+/// </summary>
+public abstract class SharedRoofSystem : EntitySystem
+{
+    [Dependency] private readonly SharedMapSystem _maps = default!;
+
+    public void SetRoof(Entity<MapGridComponent?, RoofComponent?> grid, Vector2i index, bool value)
+    {
+        if (!Resolve(grid, ref grid.Comp1, ref grid.Comp2, false))
+            return;
+
+        if (!_maps.TryGetTile(grid.Comp1, index, out var tile))
+            return;
+
+        var mask = (tile.Flags & (byte)TileFlag.Roof);
+        var rooved = mask != 0x0;
+
+        if (rooved == value)
+            return;
+
+        Tile newTile;
+
+        if (value)
+        {
+            newTile = tile.WithFlag((byte)(tile.Flags | (ushort)TileFlag.Roof));
+        }
+        else
+        {
+            newTile = tile.WithFlag((byte)(tile.Flags & ~(ushort)TileFlag.Roof));
+        }
+
+        _maps.SetTile((grid.Owner, grid.Comp1), index, newTile);
+    }
+}
index 839d920df94a629f9860309b7b07ab01458b9a7e..86ceac77be7eb63539555aff21d1104f00c9e499 100644 (file)
@@ -1,9 +1,11 @@
 using Content.Shared.Atmos;
+using Content.Shared.Light.Components;
 using Content.Shared.Movement.Systems;
 using Content.Shared.Tools;
 using Robust.Shared.Audio;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
 using Robust.Shared.Utility;
@@ -118,4 +120,11 @@ namespace Content.Shared.Maps
             TileId = id;
         }
     }
+
+    [Flags]
+    public enum TileFlag : byte
+    {
+        None = 0,
+        Roof = 1 << 0,
+    }
 }
index 0ac9f1894c74b1f5ea45246f9698f9c7a070edc7..114b6b20b92acfd8067ba55321005af8d812d65a 100644 (file)
@@ -1,28 +1,36 @@
 using Content.Shared.Maps;
 using Robust.Shared.Noise;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Shared.Parallax.Biomes.Layers;
 
 [Serializable, NetSerializable]
 public sealed partial class BiomeTileLayer : IBiomeLayer
 {
-    [DataField("noise")] public FastNoiseLite Noise { get; private set; } = new(0);
+    [DataField] public FastNoiseLite Noise { get; private set; } = new(0);
 
     /// <inheritdoc/>
-    [DataField("threshold")]
+    [DataField]
     public float Threshold { get; private set; } = 0.5f;
 
     /// <inheritdoc/>
-    [DataField("invert")] public bool Invert { get; private set; } = false;
+    [DataField] public bool Invert { get; private set; } = false;
 
     /// <summary>
     /// Which tile variants to use for this layer. Uses all of the tile's variants if none specified
     /// </summary>
-    [DataField("variants")]
+    [DataField]
     public List<byte>? Variants = null;
 
-    [DataField("tile", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ContentTileDefinition>))]
-    public string Tile = string.Empty;
+    [DataField(required: true)]
+    public ProtoId<ContentTileDefinition> Tile = string.Empty;
+
+    // TODO: Need some good engine solution to this, see FlagSerializer for what needs changing.
+    /// <summary>
+    /// Flags to set on the tile when placed.
+    /// </summary>
+    [DataField]
+    public byte Flags = 0;
 }
index 250b0f70a54e08254c65a76f35dd41ecb8994524..32a7823273f29b5e9846b8ce14dbb68fc75592f6 100644 (file)
@@ -129,7 +129,7 @@ public abstract class SharedBiomeSystem : EntitySystem
             if (layer is not BiomeTileLayer tileLayer)
                 continue;
 
-            if (TryGetTile(indices, noiseCopy, tileLayer.Invert, tileLayer.Threshold, ProtoManager.Index<ContentTileDefinition>(tileLayer.Tile), tileLayer.Variants, out tile))
+            if (TryGetTile(indices, noiseCopy, tileLayer.Invert, tileLayer.Threshold, ProtoManager.Index(tileLayer.Tile), tileLayer.Flags, tileLayer.Variants, out tile))
             {
                 return true;
             }
@@ -142,7 +142,7 @@ public abstract class SharedBiomeSystem : EntitySystem
     /// <summary>
     /// Gets the underlying biome tile, ignoring any existing tile that may be there.
     /// </summary>
-    private bool TryGetTile(Vector2i indices, FastNoiseLite noise, bool invert, float threshold, ContentTileDefinition tileDef, List<byte>? variants, [NotNullWhen(true)] out Tile? tile)
+    private bool TryGetTile(Vector2i indices, FastNoiseLite noise, bool invert, float threshold, ContentTileDefinition tileDef, byte tileFlags, List<byte>? variants, [NotNullWhen(true)] out Tile? tile)
     {
         var found = noise.GetNoise(indices.X, indices.Y);
         found = invert ? found * -1 : found;
@@ -163,7 +163,7 @@ public abstract class SharedBiomeSystem : EntitySystem
             variant = _tile.PickVariant(tileDef, (int) variantValue);
         }
 
-        tile = new Tile(tileDef.TileId, 0, variant);
+        tile = new Tile(tileDef.TileId, flags: tileFlags, variant);
         return true;
     }
 
diff --git a/Content.Shared/SharedLightCycleSystem.cs b/Content.Shared/SharedLightCycleSystem.cs
new file mode 100644 (file)
index 0000000..1ba947f
--- /dev/null
@@ -0,0 +1,116 @@
+using Content.Shared.Light.Components;
+using Robust.Shared.Map.Components;
+
+namespace Content.Shared;
+
+public abstract class SharedLightCycleSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<LightCycleComponent, MapInitEvent>(OnCycleMapInit);
+        SubscribeLocalEvent<LightCycleComponent, ComponentShutdown>(OnCycleShutdown);
+    }
+
+    protected virtual void OnCycleMapInit(Entity<LightCycleComponent> ent, ref MapInitEvent args)
+    {
+        if (TryComp(ent.Owner, out MapLightComponent? mapLight))
+        {
+            ent.Comp.OriginalColor = mapLight.AmbientLightColor;
+            Dirty(ent);
+        }
+    }
+
+    private void OnCycleShutdown(Entity<LightCycleComponent> ent, ref ComponentShutdown args)
+    {
+        if (TryComp(ent.Owner, out MapLightComponent? mapLight))
+        {
+            mapLight.AmbientLightColor = ent.Comp.OriginalColor;
+            Dirty(ent.Owner, mapLight);
+        }
+    }
+
+    public static Color GetColor(Entity<LightCycleComponent> cycle, Color color, float time)
+    {
+        if (cycle.Comp.Enabled)
+        {
+            var lightLevel = CalculateLightLevel(cycle.Comp, time);
+            var colorLevel = CalculateColorLevel(cycle.Comp, time);
+            return new Color(
+                (byte)Math.Min(255, color.RByte * colorLevel.R * lightLevel),
+                (byte)Math.Min(255, color.GByte * colorLevel.G * lightLevel),
+                (byte)Math.Min(255, color.BByte * colorLevel.B * lightLevel)
+            );
+        }
+
+        return color;
+    }
+
+    /// <summary>
+    /// Calculates light intensity as a function of time.
+    /// </summary>
+    public static double CalculateLightLevel(LightCycleComponent comp, float time)
+    {
+        var waveLength = MathF.Max(1, (float) comp.Duration.TotalSeconds);
+        var crest = MathF.Max(0f, comp.MaxLightLevel);
+        var shift = MathF.Max(0f, comp.MinLightLevel);
+        return Math.Min(comp.ClipLight, CalculateCurve(time, waveLength, crest, shift, 6));
+    }
+
+    /// <summary>
+    /// It is important to note that each color must have a different exponent, to modify how early or late one color should stand out in relation to another.
+    /// This "simulates" what the atmosphere does and is what generates the effect of dawn and dusk.
+    /// The blue component must be a cosine function with half period, so that its minimum is at dawn and dusk, generating the "warm" color corresponding to these periods.
+    /// As you can see in the values, the maximums of the function serve more to define the curve behavior,
+    /// they must be "clipped" so as not to distort the original color of the lighting. In practice, the maximum values, in fact, are the clip thresholds.
+    /// </summary>
+    public static Color CalculateColorLevel(LightCycleComponent comp, float time)
+    {
+        var waveLength = MathF.Max(1f, (float) comp.Duration.TotalSeconds);
+
+        var red = MathF.Min(comp.ClipLevel.R,
+            CalculateCurve(time,
+                waveLength,
+                MathF.Max(0f, comp.MaxLevel.R),
+                MathF.Max(0f, comp.MinLevel.R),
+                4f));
+
+        var green = MathF.Min(comp.ClipLevel.G,
+            CalculateCurve(time,
+                waveLength,
+                MathF.Max(0f, comp.MaxLevel.G),
+                MathF.Max(0f, comp.MinLevel.G),
+                10f));
+
+        var blue = MathF.Min(comp.ClipLevel.B,
+            CalculateCurve(time,
+                waveLength / 2f,
+                MathF.Max(0f, comp.MaxLevel.B),
+                MathF.Max(0f, comp.MinLevel.B),
+                2,
+                waveLength / 4f));
+
+        return new Color(red, green, blue);
+    }
+
+    /// <summary>
+    /// Generates a sinusoidal curve as a function of x (time). The other parameters serve to adjust the behavior of the curve.
+    /// </summary>
+    /// <param name="x"> It corresponds to the independent variable of the function, which in the context of this algorithm is the current time. </param>
+    /// <param name="waveLength"> It's the wavelength of the function, it can be said to be the total duration of the light cycle. </param>
+    /// <param name="crest"> It's the maximum point of the function, where it will have its greatest value. </param>
+    /// <param name="shift"> It's the vertical displacement of the function, in practice it corresponds to the minimum value of the function. </param>
+    /// <param name="exponent"> It is the exponent of the sine, serves to "flatten" the function close to its minimum points and make it "steeper" close to its maximum. </param>
+    /// <param name="phase"> It changes the phase of the wave, like a "horizontal shift". It is important to transform the sinusoidal function into cosine, when necessary. </param>
+    /// <returns> The result of the function. </returns>
+    public static float CalculateCurve(float x,
+        float waveLength,
+        float crest,
+        float shift,
+        float exponent,
+        float phase = 0)
+    {
+        var sen = MathF.Pow(MathF.Sin((MathF.PI * (phase + x)) / waveLength), exponent);
+        return (crest - shift) * sen + shift;
+    }
+}
diff --git a/Resources/Prototypes/Entities/Markers/tile.yml b/Resources/Prototypes/Entities/Markers/tile.yml
new file mode 100644 (file)
index 0000000..2ced9e9
--- /dev/null
@@ -0,0 +1,37 @@
+- type: entity
+  id: BaseRoofMarker
+  abstract: true
+  placement:
+    mode: SnapgridCenter
+  components:
+  - type: Transform
+    anchored: true
+  - type: Sprite
+    drawdepth: Overdoors
+    sprite: Markers/cross.rsi
+
+- type: entity
+  id: RoofMarker
+  name: Roof
+  suffix: Enabled
+  parent: BaseRoofMarker
+  components:
+  - type: SetRoof
+    value: true
+  - type: Sprite
+    layers:
+    - state: green
+      shader: unshaded
+
+- type: entity
+  id: NoRoofMarker
+  name: Roof
+  suffix: Disabled
+  parent: BaseRoofMarker
+  components:
+  - type: SetRoof
+    value: false
+  - type: Sprite
+    layers:
+    - state: red
+      shader: unshaded
index 57e4daed5af6a7e8a07a06ef078cb39ba6d56ef3..278517e0e6a79ed41444a37c7337f0f102f77ca9 100644 (file)
   - type: Tag
     tags:
     - Wall
-    - Diagonal    
+    - Diagonal
   - type: Sprite
     drawdepth: Walls
     sprite: Structures/Walls/plastitanium_diagonal.rsi
index 36c7b80b81bfff773bdccff98927f40f77a0e279..68dd5671a06dc9e46df8f7f8749fb0d73e1df98c 100644 (file)
@@ -7,6 +7,8 @@
     snap:
     - Wall
   components:
+  - type: TileEmission
+    color: "#FF4500"
   - type: StepTrigger
     requiredTriggeredSpeed: 0
     intersectRatio: 0.1
index 869db085970b2f3542bdb5cd38f489afe56e05a5..ade23b6f711f55f0538535ff2d6795dd406ca7d3 100644 (file)
@@ -7,6 +7,8 @@
     snap:
     - Wall
   components:
+  - type: TileEmission
+    color: "#974988"
   - type: StepTrigger
     requiredTriggeredSpeed: 0
     intersectRatio: 0.1
index 588d95f40da5d00dfe58dfd3002a9694a54901e5..45293f582f96ed329c1bac1f220b732e5baf0814 100644 (file)
     - !type:BiomeTileLayer
       threshold: -1.0
       tile: FloorAsteroidSand
+      flags: 1
 
 # Asteroid
 - type: biomeTemplate