diff --git a/Content.Client/Light/AfterLightTargetOverlay.cs b/Content.Client/Light/AfterLightTargetOverlay.cs
new file mode 100644
index 000000000000..5cfe1e993b03
--- /dev/null
+++ b/Content.Client/Light/AfterLightTargetOverlay.cs
@@ -0,0 +1,58 @@
+using System.Numerics;
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+
+namespace Content.Client.Light;
+
+///
+/// This exists just to copy to the light render target
+///
+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();
+ 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
index 000000000000..add172c6f3c4
--- /dev/null
+++ b/Content.Client/Light/BeforeLightTargetOverlay.cs
@@ -0,0 +1,51 @@
+using System.Numerics;
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+
+namespace Content.Client.Light;
+
+///
+/// Handles an enlarged lighting target so content can use large blur radii.
+///
+public sealed class BeforeLightTargetOverlay : Overlay
+{
+ public override OverlaySpace Space => OverlaySpace.BeforeLighting;
+
+ [Dependency] private readonly IClyde _clyde = default!;
+
+ public IRenderTexture EnlargedLightTarget = default!;
+ public Box2Rotated EnlargedBounds;
+
+ ///
+ /// In metres
+ ///
+ 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
index 000000000000..2da67137ed10
--- /dev/null
+++ b/Content.Client/Light/EntitySystems/PlanetLightSystem.cs
@@ -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(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();
+ _overlayMan.RemoveOverlay();
+ _overlayMan.RemoveOverlay();
+ _overlayMan.RemoveOverlay();
+ _overlayMan.RemoveOverlay();
+ }
+}
diff --git a/Content.Client/Light/EntitySystems/RoofSystem.cs b/Content.Client/Light/EntitySystems/RoofSystem.cs
new file mode 100644
index 000000000000..559eea40fc02
--- /dev/null
+++ b/Content.Client/Light/EntitySystems/RoofSystem.cs
@@ -0,0 +1,9 @@
+using Content.Shared.Light.EntitySystems;
+
+namespace Content.Client.Light.EntitySystems;
+
+///
+public sealed class RoofSystem : SharedRoofSystem
+{
+
+}
diff --git a/Content.Client/Light/LightBlurOverlay.cs b/Content.Client/Light/LightBlurOverlay.cs
new file mode 100644
index 000000000000..ae0684f9ffcc
--- /dev/null
+++ b/Content.Client/Light/LightBlurOverlay.cs
@@ -0,0 +1,44 @@
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+
+namespace Content.Client.Light;
+
+///
+/// Essentially handles blurring for content-side light overlays.
+///
+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();
+ 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
index 000000000000..9e19423cc33c
--- /dev/null
+++ b/Content.Client/Light/LightCycleSystem.cs
@@ -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;
+
+///
+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();
+ 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
index 000000000000..981edf793c8e
--- /dev/null
+++ b/Content.Client/Light/RoofOverlay.cs
@@ -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> _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();
+ _mapSystem = _entManager.System();
+ _xformSystem = _entManager.System();
+
+ 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();
+ 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
index 000000000000..bccc6fc21e67
--- /dev/null
+++ b/Content.Client/Light/TileEmissionOverlay.cs
@@ -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 _xformQuery;
+ private readonly HashSet> _entities = new();
+
+ private List> _grids = new();
+
+ public const int ContentZIndex = RoofOverlay.ContentZIndex + 1;
+
+ public TileEmissionOverlay(IEntityManager entManager)
+ {
+ IoCManager.InjectDependencies(this);
+
+ _lookup = entManager.System();
+ _mapSystem = entManager.System();
+ _xformSystem = entManager.System();
+
+ _xformQuery = entManager.GetEntityQuery();
+ 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();
+ 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);
+ }
+}
diff --git a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs
index 213da5d78626..1bfcd8ab6088 100644
--- a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs
+++ b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs
@@ -39,12 +39,12 @@ await server.WaitAssertion(() =>
{
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 @@ await server.WaitAssertion(() =>
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 @@ await server.WaitAssertion(() =>
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
index 000000000000..6bfe64aa03dd
--- /dev/null
+++ b/Content.Server/Light/Components/SetRoofComponent.cs
@@ -0,0 +1,11 @@
+namespace Content.Server.Light.Components;
+
+///
+/// Applies the roof flag to this tile and deletes the entity.
+///
+[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
index 000000000000..7d2eacc8bbbe
--- /dev/null
+++ b/Content.Server/Light/EntitySystems/LightCycleSystem.cs
@@ -0,0 +1,22 @@
+using Content.Shared;
+using Content.Shared.Light.Components;
+using Robust.Shared.Random;
+
+namespace Content.Server.Light.EntitySystems;
+
+///
+public sealed class LightCycleSystem : SharedLightCycleSystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ protected override void OnCycleMapInit(Entity 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
index 000000000000..a3b8cb183618
--- /dev/null
+++ b/Content.Server/Light/EntitySystems/RoofSystem.cs
@@ -0,0 +1,33 @@
+using Content.Server.Light.Components;
+using Content.Shared.Light.EntitySystems;
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Light.EntitySystems;
+
+///
+public sealed class RoofSystem : SharedRoofSystem
+{
+ [Dependency] private readonly SharedMapSystem _maps = default!;
+
+ private EntityQuery _gridQuery;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _gridQuery = GetEntityQuery();
+ SubscribeLocalEvent(OnFlagStartup);
+ }
+
+ private void OnFlagStartup(Entity 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);
+ }
+}
diff --git a/Content.Server/Parallax/BiomeSystem.cs b/Content.Server/Parallax/BiomeSystem.cs
index 109aa0f6e470..e419e90fd38d 100644
--- a/Content.Server/Parallax/BiomeSystem.cs
+++ b/Content.Server/Parallax/BiomeSystem.cs
@@ -12,6 +12,7 @@
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 override void Update(float frameTime)
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 override void Update(float frameTime)
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 @@ private void LoadChunkMarkers(
}
if (modified.Count == 0)
+ {
+ component.ModifiedTiles.Remove(chunk);
_tilePool.Return(modified);
+ }
component.PendingMarkers.Remove(chunk);
}
@@ -1014,11 +1025,14 @@ public void EnsurePlanet(EntityUid mapUid, BiomeTemplatePrototype biomeTemplate,
// Midday: #E6CB8B
// Moonlight: #2b3143
// Lava: #A34931
-
var light = EnsureComp(mapUid);
light.AmbientLightColor = mapLight ?? Color.FromHex("#D8B059");
Dirty(mapUid, light, metadata);
+ EnsureComp(mapUid);
+
+ EnsureComp(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
index 000000000000..a6ce63bdbc2b
--- /dev/null
+++ b/Content.Shared/Light/Components/LightCycleComponent.cs
@@ -0,0 +1,56 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Map.Components;
+
+namespace Content.Shared.Light.Components;
+
+///
+/// Cycles through colors AKA "Day / Night cycle" on
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class LightCycleComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public Color OriginalColor = Color.Transparent;
+
+ ///
+ /// How long an entire cycle lasts
+ ///
+ [DataField, AutoNetworkedField]
+ public TimeSpan Duration = TimeSpan.FromMinutes(30);
+
+ [DataField, AutoNetworkedField]
+ public TimeSpan Offset;
+
+ [DataField, AutoNetworkedField]
+ public bool Enabled = true;
+
+ ///
+ /// Should the offset be randomised upon MapInit.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool InitialOffset = true;
+
+ ///
+ /// Trench of the oscillation.
+ ///
+ [DataField, AutoNetworkedField]
+ public float MinLightLevel = 0f;
+
+ ///
+ /// Peak of the oscillation
+ ///
+ [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
index 000000000000..0e2adf527cda
--- /dev/null
+++ b/Content.Shared/Light/Components/RoofComponent.cs
@@ -0,0 +1,13 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Light.Components;
+
+///
+/// Will draw shadows over tiles flagged as roof tiles on the attached map.
+///
+[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
index 000000000000..0eec19702d81
--- /dev/null
+++ b/Content.Shared/Light/Components/TileEmissionComponent.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Light.Components;
+
+///
+/// Will draw lighting in a range around the tile.
+///
+[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
index 000000000000..d06b5bcb0ce0
--- /dev/null
+++ b/Content.Shared/Light/EntitySystems/SharedRoofSystem.cs
@@ -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;
+
+///
+/// Handles the roof flag for tiles that gets used for the RoofOverlay.
+///
+public abstract class SharedRoofSystem : EntitySystem
+{
+ [Dependency] private readonly SharedMapSystem _maps = default!;
+
+ public void SetRoof(Entity 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);
+ }
+}
diff --git a/Content.Shared/Maps/ContentTileDefinition.cs b/Content.Shared/Maps/ContentTileDefinition.cs
index 839d920df94a..86ceac77be7e 100644
--- a/Content.Shared/Maps/ContentTileDefinition.cs
+++ b/Content.Shared/Maps/ContentTileDefinition.cs
@@ -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 @@ public void AssignTileId(ushort id)
TileId = id;
}
}
+
+ [Flags]
+ public enum TileFlag : byte
+ {
+ None = 0,
+ Roof = 1 << 0,
+ }
}
diff --git a/Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs b/Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs
index 0ac9f1894c74..114b6b20b92a 100644
--- a/Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs
+++ b/Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs
@@ -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);
///
- [DataField("threshold")]
+ [DataField]
public float Threshold { get; private set; } = 0.5f;
///
- [DataField("invert")] public bool Invert { get; private set; } = false;
+ [DataField] public bool Invert { get; private set; } = false;
///
/// Which tile variants to use for this layer. Uses all of the tile's variants if none specified
///
- [DataField("variants")]
+ [DataField]
public List? Variants = null;
- [DataField("tile", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
- public string Tile = string.Empty;
+ [DataField(required: true)]
+ public ProtoId Tile = string.Empty;
+
+ // TODO: Need some good engine solution to this, see FlagSerializer for what needs changing.
+ ///
+ /// Flags to set on the tile when placed.
+ ///
+ [DataField]
+ public byte Flags = 0;
}
diff --git a/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs b/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs
index 250b0f70a54e..32a7823273f2 100644
--- a/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs
+++ b/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs
@@ -129,7 +129,7 @@ public bool TryGetTile(Vector2i indices, List layers, int seed, Map
if (layer is not BiomeTileLayer tileLayer)
continue;
- if (TryGetTile(indices, noiseCopy, tileLayer.Invert, tileLayer.Threshold, ProtoManager.Index(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 bool TryGetTile(Vector2i indices, List layers, int seed, Map
///
/// Gets the underlying biome tile, ignoring any existing tile that may be there.
///
- private bool TryGetTile(Vector2i indices, FastNoiseLite noise, bool invert, float threshold, ContentTileDefinition tileDef, List? variants, [NotNullWhen(true)] out Tile? tile)
+ private bool TryGetTile(Vector2i indices, FastNoiseLite noise, bool invert, float threshold, ContentTileDefinition tileDef, byte tileFlags, List? variants, [NotNullWhen(true)] out Tile? tile)
{
var found = noise.GetNoise(indices.X, indices.Y);
found = invert ? found * -1 : found;
@@ -163,7 +163,7 @@ private bool TryGetTile(Vector2i indices, FastNoiseLite noise, bool invert, floa
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
index 000000000000..1ba947f78ac3
--- /dev/null
+++ b/Content.Shared/SharedLightCycleSystem.cs
@@ -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(OnCycleMapInit);
+ SubscribeLocalEvent(OnCycleShutdown);
+ }
+
+ protected virtual void OnCycleMapInit(Entity ent, ref MapInitEvent args)
+ {
+ if (TryComp(ent.Owner, out MapLightComponent? mapLight))
+ {
+ ent.Comp.OriginalColor = mapLight.AmbientLightColor;
+ Dirty(ent);
+ }
+ }
+
+ private void OnCycleShutdown(Entity 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 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;
+ }
+
+ ///
+ /// Calculates light intensity as a function of time.
+ ///
+ 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));
+ }
+
+ ///
+ /// 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.
+ ///
+ 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);
+ }
+
+ ///
+ /// Generates a sinusoidal curve as a function of x (time). The other parameters serve to adjust the behavior of the curve.
+ ///
+ /// It corresponds to the independent variable of the function, which in the context of this algorithm is the current time.
+ /// It's the wavelength of the function, it can be said to be the total duration of the light cycle.
+ /// It's the maximum point of the function, where it will have its greatest value.
+ /// It's the vertical displacement of the function, in practice it corresponds to the minimum value of the function.
+ /// 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.
+ /// It changes the phase of the wave, like a "horizontal shift". It is important to transform the sinusoidal function into cosine, when necessary.
+ /// The result of the function.
+ 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
index 000000000000..2ced9e9958de
--- /dev/null
+++ b/Resources/Prototypes/Entities/Markers/tile.yml
@@ -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
diff --git a/Resources/Prototypes/Entities/Structures/Walls/walls.yml b/Resources/Prototypes/Entities/Structures/Walls/walls.yml
index 57e4daed5af6..278517e0e6a7 100644
--- a/Resources/Prototypes/Entities/Structures/Walls/walls.yml
+++ b/Resources/Prototypes/Entities/Structures/Walls/walls.yml
@@ -543,7 +543,7 @@
- type: Tag
tags:
- Wall
- - Diagonal
+ - Diagonal
- type: Sprite
drawdepth: Walls
sprite: Structures/Walls/plastitanium_diagonal.rsi
diff --git a/Resources/Prototypes/Entities/Tiles/lava.yml b/Resources/Prototypes/Entities/Tiles/lava.yml
index 36c7b80b81bf..68dd5671a06d 100644
--- a/Resources/Prototypes/Entities/Tiles/lava.yml
+++ b/Resources/Prototypes/Entities/Tiles/lava.yml
@@ -7,6 +7,8 @@
snap:
- Wall
components:
+ - type: TileEmission
+ color: "#FF4500"
- type: StepTrigger
requiredTriggeredSpeed: 0
intersectRatio: 0.1
diff --git a/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml b/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml
index 869db085970b..ade23b6f711f 100644
--- a/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml
+++ b/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml
@@ -7,6 +7,8 @@
snap:
- Wall
components:
+ - type: TileEmission
+ color: "#974988"
- type: StepTrigger
requiredTriggeredSpeed: 0
intersectRatio: 0.1
diff --git a/Resources/Prototypes/Procedural/biome_templates.yml b/Resources/Prototypes/Procedural/biome_templates.yml
index 588d95f40da5..45293f582f96 100644
--- a/Resources/Prototypes/Procedural/biome_templates.yml
+++ b/Resources/Prototypes/Procedural/biome_templates.yml
@@ -544,6 +544,7 @@
- !type:BiomeTileLayer
threshold: -1.0
tile: FloorAsteroidSand
+ flags: 1
# Asteroid
- type: biomeTemplate