]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add sun shadows (planet lighting stage 2) (#35145)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Sat, 8 Mar 2025 05:07:42 +0000 (16:07 +1100)
committerGitHub <noreply@github.com>
Sat, 8 Mar 2025 05:07:42 +0000 (16:07 +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

* Apply RoofOverlay per-grid not per-map

* Fix light render scales

* sangas

* Juice it a bit

* Better angle

* Fixes

* Add color support

* Rounding bandaid

* Wehs

* Better

* Remember I forgot to do this when writing docs

---------

Co-authored-by: DoutorWhite <thedoctorwhite@gmail.com>
17 files changed:
Content.Client/Light/AfterLightTargetOverlay.cs
Content.Client/Light/EntitySystems/PlanetLightSystem.cs
Content.Client/Light/EntitySystems/SunShadowSystem.cs [new file with mode: 0644]
Content.Client/Light/LightCycleSystem.cs
Content.Client/Light/RoofOverlay.cs
Content.Client/Light/SunShadowOverlay.cs [new file with mode: 0644]
Content.Server/Light/EntitySystems/LightCycleSystem.cs
Content.Server/Light/EntitySystems/SunShadowSystem.cs [new file with mode: 0644]
Content.Server/Parallax/BiomeSystem.cs
Content.Shared/Light/Components/IsRoofComponent.cs
Content.Shared/Light/Components/SunShadowCastComponent.cs [new file with mode: 0644]
Content.Shared/Light/Components/SunShadowComponent.cs [new file with mode: 0644]
Content.Shared/Light/Components/SunShadowCycleComponent.cs [new file with mode: 0644]
Content.Shared/Light/EntitySystems/SharedLightCycleSystem.cs [moved from Content.Shared/SharedLightCycleSystem.cs with 90% similarity]
Content.Shared/Light/EntitySystems/SharedRoofSystem.cs
Content.Shared/Light/EntitySystems/SharedSunShadowSystem.cs [new file with mode: 0644]
Resources/Prototypes/Entities/Structures/Walls/walls.yml

index 06c508a54ee52b7d6369b18b0989c1ac5d75a888..7856fd4ded07ed93a6351adfb57d0de3b4f6f9b0 100644 (file)
@@ -54,6 +54,6 @@ public sealed class AfterLightTargetOverlay : Overlay
 
                 worldHandle.SetTransform(localMatrix);
                 worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
-            }, null);
+            }, Color.Transparent);
     }
 }
index 2da67137ed1024f476c254b0b1ec9c577892a9c4..cbe2f47f78e5be5539d57102faa7ce2617029a72 100644 (file)
@@ -16,6 +16,7 @@ public sealed class PlanetLightSystem : EntitySystem
         _overlayMan.AddOverlay(new RoofOverlay(EntityManager));
         _overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));
         _overlayMan.AddOverlay(new LightBlurOverlay());
+        _overlayMan.AddOverlay(new SunShadowOverlay());
         _overlayMan.AddOverlay(new AfterLightTargetOverlay());
     }
 
@@ -31,6 +32,7 @@ public sealed class PlanetLightSystem : EntitySystem
         _overlayMan.RemoveOverlay<RoofOverlay>();
         _overlayMan.RemoveOverlay<TileEmissionOverlay>();
         _overlayMan.RemoveOverlay<LightBlurOverlay>();
+        _overlayMan.RemoveOverlay<SunShadowOverlay>();
         _overlayMan.RemoveOverlay<AfterLightTargetOverlay>();
     }
 }
diff --git a/Content.Client/Light/EntitySystems/SunShadowSystem.cs b/Content.Client/Light/EntitySystems/SunShadowSystem.cs
new file mode 100644 (file)
index 0000000..6f7a965
--- /dev/null
@@ -0,0 +1,92 @@
+using System.Diagnostics.Contracts;
+using System.Numerics;
+using Content.Client.GameTicking.Managers;
+using Content.Shared.Light.Components;
+using Content.Shared.Light.EntitySystems;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Light.EntitySystems;
+
+public sealed class SunShadowSystem : SharedSunShadowSystem
+{
+    [Dependency] private readonly ClientGameTicker _ticker = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly MetaDataSystem _metadata = default!;
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        if (!_timing.IsFirstTimePredicted)
+            return;
+
+        var mapQuery = AllEntityQuery<SunShadowCycleComponent, SunShadowComponent>();
+        while (mapQuery.MoveNext(out var uid,  out var cycle, out var shadow))
+        {
+            if (!cycle.Running || cycle.Directions.Count == 0)
+                continue;
+
+            var pausedTime = _metadata.GetPauseTime(uid);
+
+            var time = (float)(_timing.CurTime
+                .Add(cycle.Offset)
+                .Subtract(_ticker.RoundStartTimeSpan)
+                .Subtract(pausedTime)
+                .TotalSeconds % cycle.Duration.TotalSeconds);
+
+            var (direction, alpha) = GetShadow((uid, cycle), time);
+            shadow.Direction = direction;
+            shadow.Alpha = alpha;
+        }
+    }
+
+    [Pure]
+    public (Vector2 Direction, float Alpha) GetShadow(Entity<SunShadowCycleComponent> entity, float time)
+    {
+        // So essentially the values are stored as the percentages of the total duration just so it adjusts the speed
+        // dynamically and we don't have to manually handle it.
+        // It will lerp from each value to the next one with angle and length handled separately
+        var ratio = (float) (time / entity.Comp.Duration.TotalSeconds);
+
+        for (var i = entity.Comp.Directions.Count - 1; i >= 0; i--)
+        {
+            var dir = entity.Comp.Directions[i];
+
+            if (ratio > dir.Ratio)
+            {
+                var next = entity.Comp.Directions[(i + 1) % entity.Comp.Directions.Count];
+                float nextRatio;
+
+                // Last entry
+                if (i == entity.Comp.Directions.Count - 1)
+                {
+                    nextRatio = next.Ratio + 1f;
+                }
+                else
+                {
+                    nextRatio = next.Ratio;
+                }
+
+                var range = nextRatio - dir.Ratio;
+                var diff = (ratio - dir.Ratio) / range;
+                DebugTools.Assert(diff is >= 0f and <= 1f);
+
+                // We lerp angle + length separately as we don't want a straight-line lerp and want the rotation to be consistent.
+                var currentAngle = dir.Direction.ToAngle();
+                var nextAngle = next.Direction.ToAngle();
+
+                var angle = Angle.Lerp(currentAngle, nextAngle, diff);
+                // This is to avoid getting weird issues where the angle gets pretty close but length still noticeably catches up.
+                var lengthDiff = MathF.Pow(diff, 1f / 2f);
+                var length = float.Lerp(dir.Direction.Length(), next.Direction.Length(), lengthDiff);
+
+                var vector = angle.ToVec() * length;
+                var alpha = float.Lerp(dir.Alpha, next.Alpha, diff);
+                return (vector, alpha);
+            }
+        }
+
+        throw new InvalidOperationException();
+    }
+}
index 9e19423cc33c9f0114f58415cf8b1df3e376677f..8de0165fd24466102d771991ba2a7aef15c6f1a0 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Client.GameTicking.Managers;
 using Content.Shared;
 using Content.Shared.Light.Components;
+using Content.Shared.Light.EntitySystems;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Timing;
 
@@ -11,19 +12,29 @@ public sealed class LightCycleSystem : SharedLightCycleSystem
 {
     [Dependency] private readonly ClientGameTicker _ticker = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly MetaDataSystem _metadata = default!;
 
     public override void Update(float frameTime)
     {
         base.Update(frameTime);
+
+        if (!_timing.IsFirstTimePredicted)
+            return;
+
         var mapQuery = AllEntityQuery<LightCycleComponent, MapLightComponent>();
         while (mapQuery.MoveNext(out var uid,  out var cycle, out var map))
         {
             if (!cycle.Running)
                 continue;
 
+            // We still iterate paused entities as we still want to override the lighting color and not have
+            // it apply the server state
+            var pausedTime = _metadata.GetPauseTime(uid);
+
             var time = (float) _timing.CurTime
                 .Add(cycle.Offset)
                 .Subtract(_ticker.RoundStartTimeSpan)
+                .Subtract(pausedTime)
                 .TotalSeconds;
 
             var color = GetColor((uid, cycle), cycle.OriginalColor, time);
index 0648f8624fae3f090765bd77ff0d97504d30d487..894463016936be9a2163132fb0a92bf0e364e058 100644 (file)
@@ -94,13 +94,15 @@ public sealed class RoofOverlay : Overlay
                     // Due to stencilling we essentially draw on unrooved tiles
                     while (tileEnumerator.MoveNext(out var tileRef))
                     {
-                        if (!_roof.IsRooved(roofEnt, tileRef.GridIndices))
+                        var color = _roof.GetColor(roofEnt, tileRef.GridIndices);
+
+                        if (color == null)
                         {
                             continue;
                         }
 
                         var local = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize);
-                        worldHandle.DrawRect(local, roof.Color);
+                        worldHandle.DrawRect(local, color.Value);
                     }
                 }
             }, null);
diff --git a/Content.Client/Light/SunShadowOverlay.cs b/Content.Client/Light/SunShadowOverlay.cs
new file mode 100644 (file)
index 0000000..de8b5ed
--- /dev/null
@@ -0,0 +1,154 @@
+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;
+using Robust.Shared.Physics;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Light;
+
+public sealed class SunShadowOverlay : Overlay
+{
+    public override OverlaySpace Space => OverlaySpace.BeforeLighting;
+
+    [Dependency] private readonly IClyde _clyde = default!;
+    [Dependency] private readonly IEntityManager _entManager = default!;
+    [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly IPrototypeManager _protoManager = default!;
+    private readonly EntityLookupSystem _lookup;
+    private readonly SharedTransformSystem _xformSys;
+
+    private readonly HashSet<Entity<SunShadowCastComponent>> _shadows = new();
+
+    private IRenderTexture? _blurTarget;
+    private IRenderTexture? _target;
+
+    public SunShadowOverlay()
+    {
+        IoCManager.InjectDependencies(this);
+        _xformSys = _entManager.System<SharedTransformSystem>();
+        _lookup = _entManager.System<EntityLookupSystem>();
+        ZIndex = AfterLightTargetOverlay.ContentZIndex + 1;
+    }
+
+    private List<Entity<MapGridComponent>> _grids = new();
+
+    protected override void Draw(in OverlayDrawArgs args)
+    {
+        var viewport = args.Viewport;
+        var eye = viewport.Eye;
+
+        if (eye == null)
+            return;
+
+        _grids.Clear();
+        _mapManager.FindGridsIntersecting(args.MapId,
+            args.WorldBounds.Enlarged(SunShadowComponent.MaxLength),
+            ref _grids);
+
+        var worldHandle = args.WorldHandle;
+        var mapId = args.MapId;
+        var worldBounds = args.WorldBounds;
+        var targetSize = viewport.LightRenderTarget.Size;
+
+        if (_target?.Size != targetSize)
+        {
+            _target = _clyde
+                .CreateRenderTarget(targetSize,
+                    new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
+                    name: "sun-shadow-target");
+
+            if (_blurTarget?.Size != targetSize)
+            {
+                _blurTarget = _clyde
+                    .CreateRenderTarget(targetSize, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "sun-shadow-blur");
+            }
+        }
+
+        var lightScale = viewport.LightRenderTarget.Size / (Vector2)viewport.Size;
+        var scale = viewport.RenderScale / (Vector2.One / lightScale);
+
+        foreach (var grid in _grids)
+        {
+            if (!_entManager.TryGetComponent(grid.Owner, out SunShadowComponent? sun))
+            {
+                continue;
+            }
+
+            var direction = sun.Direction;
+            var alpha = Math.Clamp(sun.Alpha, 0f, 1f);
+
+            // Nowhere to cast to so ignore it.
+            if (direction.Equals(Vector2.Zero) || alpha == 0f)
+                continue;
+
+            // Feature todo: dynamic shadows for mobs and trees. Also ideally remove the fake tree shadows.
+            // TODO: Jittering still not quite perfect
+
+            var expandedBounds = worldBounds.Enlarged(direction.Length() + 0.01f);
+            _shadows.Clear();
+
+            // Draw shadow polys to stencil
+            args.WorldHandle.RenderInRenderTarget(_target,
+                () =>
+                {
+                    var invMatrix =
+                        _target.GetWorldToLocalMatrix(eye, scale);
+                    var indices = new Vector2[PhysicsConstants.MaxPolygonVertices * 2];
+
+                    // Go through shadows in range.
+
+                    // For each one we:
+                    // - Get the original vertices.
+                    // - Extrapolate these along the sun direction.
+                    // - Combine the above into 1 single polygon to draw.
+
+                    // Note that this is range-limited for accuracy; if you set it too high it will clip through walls or other undesirable entities.
+                    // This is probably not noticeable most of the time but if you want something "accurate" you'll want to code a solution.
+                    // Ideally the CPU would have its own shadow-map copy that we could just ray-cast each vert into though
+                    // You might need to batch verts or the likes as this could get expensive.
+                    _lookup.GetEntitiesIntersecting(mapId, expandedBounds, _shadows);
+
+                    foreach (var ent in _shadows)
+                    {
+                        var xform = _entManager.GetComponent<TransformComponent>(ent.Owner);
+                        var worldMatrix = _xformSys.GetWorldMatrix(xform);
+                        var renderMatrix = Matrix3x2.Multiply(worldMatrix, invMatrix);
+                        var pointCount = ent.Comp.Points.Length;
+
+                        Array.Copy(ent.Comp.Points, indices, pointCount);
+
+                        for (var i = 0; i < pointCount; i++)
+                        {
+                            indices[pointCount + i] = indices[i] + direction;
+                        }
+
+                        var points = PhysicsHull.ComputePoints(indices, pointCount * 2);
+                        worldHandle.SetTransform(renderMatrix);
+
+                        worldHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, points, Color.White);
+                    }
+                },
+                Color.Transparent);
+
+            // Slightly blur it just to avoid aliasing issues on the later viewport-wide blur.
+            _clyde.BlurRenderTarget(viewport, _target, _target, eye, 1f);
+
+            // Draw stencil (see roofoverlay).
+            args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
+                () =>
+                {
+                    var invMatrix =
+                        viewport.LightRenderTarget.GetWorldToLocalMatrix(eye, scale);
+                    worldHandle.SetTransform(invMatrix);
+
+                    var maskShader = _protoManager.Index<ShaderPrototype>("Mix").Instance();
+                    worldHandle.UseShader(maskShader);
+
+                    worldHandle.DrawTextureRect(_target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
+                }, null);
+        }
+    }
+}
index 7d2eacc8bbbe44b4c57c84f9564805da4a321f75..89863ed3dc2f55d47bde2287d029c958d6daef7b 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Shared;
 using Content.Shared.Light.Components;
+using Content.Shared.Light.EntitySystems;
 using Robust.Shared.Random;
 
 namespace Content.Server.Light.EntitySystems;
@@ -15,8 +16,7 @@ public sealed class LightCycleSystem : SharedLightCycleSystem
 
         if (ent.Comp.InitialOffset)
         {
-            ent.Comp.Offset = _random.Next(ent.Comp.Duration);
-            Dirty(ent);
+            SetOffset(ent, _random.Next(ent.Comp.Duration));
         }
     }
 }
diff --git a/Content.Server/Light/EntitySystems/SunShadowSystem.cs b/Content.Server/Light/EntitySystems/SunShadowSystem.cs
new file mode 100644 (file)
index 0000000..87ccfeb
--- /dev/null
@@ -0,0 +1,8 @@
+using Content.Shared.Light.EntitySystems;
+
+namespace Content.Server.Light.EntitySystems;
+
+public sealed class SunShadowSystem : SharedSunShadowSystem
+{
+
+}
index e419e90fd38d783bb39b6b20aab783605d71e0e9..89aa7548506822715117ee38787b0eb7ddc519f6 100644 (file)
@@ -1033,6 +1033,9 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
 
         EnsureComp<LightCycleComponent>(mapUid);
 
+        EnsureComp<SunShadowComponent>(mapUid);
+        EnsureComp<SunShadowCycleComponent>(mapUid);
+
         var moles = new float[Atmospherics.AdjustedNumberOfGases];
         moles[(int) Gas.Oxygen] = 21.824779f;
         moles[(int) Gas.Nitrogen] = 82.10312f;
index d64793f3586b7ec1b61a6f7fe6d7dbe0144fd7fd..624ad63e55d07b7136f872af7f9fc3ebe31dfec4 100644 (file)
@@ -10,4 +10,13 @@ public sealed partial class IsRoofComponent : Component
 {
     [DataField, AutoNetworkedField]
     public bool Enabled = true;
+
+    /// <summary>
+    /// Color for this roof. If null then falls back to the grid's color.
+    /// </summary>
+    /// <remarks>
+    /// If a tile is marked as rooved then the tile color will be used over any entity's colors on the tile.
+    /// </remarks>
+    [DataField, AutoNetworkedField]
+    public Color? Color;
 }
diff --git a/Content.Shared/Light/Components/SunShadowCastComponent.cs b/Content.Shared/Light/Components/SunShadowCastComponent.cs
new file mode 100644 (file)
index 0000000..d3fc6dc
--- /dev/null
@@ -0,0 +1,25 @@
+using System.Numerics;
+using Robust.Shared.GameStates;
+using Robust.Shared.Physics;
+
+namespace Content.Shared.Light.Components;
+
+/// <summary>
+/// Treats this entity as a 1x1 tile and extrapolates its position along the <see cref="SunShadowComponent"/> direction.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SunShadowCastComponent : Component
+{
+    /// <summary>
+    /// Points that will be extruded to draw the shadow color.
+    /// Max <see cref="PhysicsConstants.MaxPolygonVertices"/>
+    /// </summary>
+    [DataField]
+    public Vector2[] Points = new[]
+    {
+        new Vector2(-0.5f, -0.5f),
+        new Vector2(0.5f, -0.5f),
+        new Vector2(0.5f, 0.5f),
+        new Vector2(-0.5f, 0.5f),
+    };
+}
diff --git a/Content.Shared/Light/Components/SunShadowComponent.cs b/Content.Shared/Light/Components/SunShadowComponent.cs
new file mode 100644 (file)
index 0000000..f7f4271
--- /dev/null
@@ -0,0 +1,25 @@
+using System.Numerics;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Light.Components;
+
+/// <summary>
+/// When added to a map will apply shadows from <see cref="SunShadowComponent"/> to the lighting render target.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class SunShadowComponent : Component
+{
+    /// <summary>
+    /// Maximum length of <see cref="Direction"/>. Mostly used in context of querying for grids off-screen.
+    /// </summary>
+    public const float MaxLength = 5f;
+
+    /// <summary>
+    /// Direction for the shadows to be extrapolated in.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public Vector2 Direction;
+
+    [DataField, AutoNetworkedField]
+    public float Alpha;
+}
diff --git a/Content.Shared/Light/Components/SunShadowCycleComponent.cs b/Content.Shared/Light/Components/SunShadowCycleComponent.cs
new file mode 100644 (file)
index 0000000..0948091
--- /dev/null
@@ -0,0 +1,35 @@
+using System.Linq;
+using System.Numerics;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Light.Components;
+
+/// <summary>
+/// Applies <see cref="SunShadowComponent"/> direction vectors based on a time-offset. Will track <see cref="LightCycleComponent"/> on on MapInit
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class SunShadowCycleComponent : Component
+{
+    /// <summary>
+    /// How long an entire cycle lasts
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public TimeSpan Duration = TimeSpan.FromMinutes(30);
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan Offset;
+
+    // Originally had this as ratios but it was slightly annoying to use.
+
+    /// <summary>
+    /// Time to have each direction applied. Will lerp from the current value to the next one.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public List<(float Ratio, Vector2 Direction, float Alpha)> Directions = new()
+    {
+        (0f, new Vector2(0f, 3f), 0f),
+        (0.25f, new Vector2(-3f, -0.1f), 0.5f),
+        (0.5f, new Vector2(0f, -3f), 0.8f),
+        (0.75f, new Vector2(3f, -0.1f), 0.5f),
+    };
+}
similarity index 90%
rename from Content.Shared/SharedLightCycleSystem.cs
rename to Content.Shared/Light/EntitySystems/SharedLightCycleSystem.cs
index 1ba947f78ac337b6fcf6e1712d3c285ecda44eb3..3b3d6d702c63e53e1752c8ea82ee39a1343c7f8e 100644 (file)
@@ -1,7 +1,7 @@
 using Content.Shared.Light.Components;
 using Robust.Shared.Map.Components;
 
-namespace Content.Shared;
+namespace Content.Shared.Light.EntitySystems;
 
 public abstract class SharedLightCycleSystem : EntitySystem
 {
@@ -30,6 +30,15 @@ public abstract class SharedLightCycleSystem : EntitySystem
         }
     }
 
+    public void SetOffset(Entity<LightCycleComponent> entity, TimeSpan offset)
+    {
+        entity.Comp.Offset = offset;
+        var ev = new LightCycleOffsetEvent(offset);
+
+        RaiseLocalEvent(entity, ref ev);
+        Dirty(entity);
+    }
+
     public static Color GetColor(Entity<LightCycleComponent> cycle, Color color, float time)
     {
         if (cycle.Comp.Enabled)
@@ -114,3 +123,12 @@ public abstract class SharedLightCycleSystem : EntitySystem
         return (crest - shift) * sen + shift;
     }
 }
+
+/// <summary>
+/// Raised when the offset on <see cref="LightCycleComponent"/> changes.
+/// </summary>
+[ByRefEvent]
+public record struct LightCycleOffsetEvent(TimeSpan Offset)
+{
+    public readonly TimeSpan Offset = Offset;
+}
index 2d19b8ba87cec91dc7eefe64105f5cd32479787c..46ec41857912466a53cd47059673a0a0d05bd9e0 100644 (file)
@@ -1,3 +1,4 @@
+using System.Diagnostics.Contracts;
 using Content.Shared.Light.Components;
 using Content.Shared.Maps;
 using Robust.Shared.Map;
@@ -18,6 +19,7 @@ public abstract class SharedRoofSystem : EntitySystem
     /// Returns whether the specified tile is roof-occupied.
     /// </summary>
     /// <returns>Returns false if no data or not rooved.</returns>
+    [Pure]
     public bool IsRooved(Entity<MapGridComponent, RoofComponent> grid, Vector2i index)
     {
         var roof = grid.Comp2;
@@ -49,6 +51,40 @@ public abstract class SharedRoofSystem : EntitySystem
         return false;
     }
 
+    [Pure]
+    public Color? GetColor(Entity<MapGridComponent, RoofComponent> grid, Vector2i index)
+    {
+        var roof = grid.Comp2;
+        var chunkOrigin = SharedMapSystem.GetChunkIndices(index, RoofComponent.ChunkSize);
+
+        if (roof.Data.TryGetValue(chunkOrigin, out var bitMask))
+        {
+            var chunkRelative = SharedMapSystem.GetChunkRelative(index, RoofComponent.ChunkSize);
+            var bitFlag = (ulong) 1 << (chunkRelative.X + chunkRelative.Y * RoofComponent.ChunkSize);
+
+            var isRoof = (bitMask & bitFlag) == bitFlag;
+
+            // Early out, otherwise check for components on tile.
+            if (isRoof)
+            {
+                return roof.Color;
+            }
+        }
+
+        _roofSet.Clear();
+        _lookup.GetLocalEntitiesIntersecting(grid.Owner, index, _roofSet);
+
+        foreach (var isRoofEnt in _roofSet)
+        {
+            if (!isRoofEnt.Comp.Enabled)
+                continue;
+
+            return isRoofEnt.Comp.Color ?? roof.Color;
+        }
+
+        return null;
+    }
+
     public void SetRoof(Entity<MapGridComponent?, RoofComponent?> grid, Vector2i index, bool value)
     {
         if (!Resolve(grid, ref grid.Comp1, ref grid.Comp2, false))
diff --git a/Content.Shared/Light/EntitySystems/SharedSunShadowSystem.cs b/Content.Shared/Light/EntitySystems/SharedSunShadowSystem.cs
new file mode 100644 (file)
index 0000000..ed6069d
--- /dev/null
@@ -0,0 +1,39 @@
+using Content.Shared.Light.Components;
+using Robust.Shared.Random;
+
+namespace Content.Shared.Light.EntitySystems;
+
+public abstract class SharedSunShadowSystem : EntitySystem
+{
+    [Dependency] private readonly IRobustRandom _random = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<SunShadowCycleComponent, MapInitEvent>(OnCycleMapInit);
+        SubscribeLocalEvent<SunShadowCycleComponent, LightCycleOffsetEvent>(OnCycleOffset);
+    }
+
+    private void OnCycleOffset(Entity<SunShadowCycleComponent> ent, ref LightCycleOffsetEvent args)
+    {
+        // Okay so we synchronise with LightCycleComponent.
+        // However, the offset is only set on MapInit and we have no guarantee which one is ran first so we make sure.
+        ent.Comp.Offset = args.Offset;
+        Dirty(ent);
+    }
+
+    private void OnCycleMapInit(Entity<SunShadowCycleComponent> ent, ref MapInitEvent args)
+    {
+        if (TryComp(ent.Owner, out LightCycleComponent? lightCycle))
+        {
+            ent.Comp.Duration = lightCycle.Duration;
+            ent.Comp.Offset = lightCycle.Offset;
+        }
+        else
+        {
+            ent.Comp.Offset = _random.Next(ent.Comp.Duration);
+        }
+
+        Dirty(ent);
+    }
+}
index dd6b3d36bc1af1f728fd32b79ebda6065d8f58e6..57b8aa915e72def473a5daea1501b509197b8d01 100644 (file)
@@ -51,6 +51,7 @@
   - type: RadiationBlocker
     resistance: 2
   - type: BlockWeather
+  - type: SunShadowCast
 
 - type: entity
   parent: BaseWall