diff --git a/Content.Client/Atmos/Components/MapAtmosphereComponent.cs b/Content.Client/Atmos/Components/MapAtmosphereComponent.cs new file mode 100644 index 0000000000..2f59eae439 --- /dev/null +++ b/Content.Client/Atmos/Components/MapAtmosphereComponent.cs @@ -0,0 +1,9 @@ +using Content.Shared.Atmos.Components; + +namespace Content.Client.Atmos.Components; + +[RegisterComponent] +public sealed class MapAtmosphereComponent : SharedMapAtmosphereComponent +{ + +} diff --git a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs index d7f9d60d83..44759372f4 100644 --- a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs +++ b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs @@ -1,10 +1,24 @@ -using Content.Shared.Atmos.EntitySystems; -using JetBrains.Annotations; +using Content.Client.Atmos.Components; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; +using Robust.Shared.GameStates; -namespace Content.Client.Atmos.EntitySystems +namespace Content.Client.Atmos.EntitySystems; + +public sealed class AtmosphereSystem : SharedAtmosphereSystem { - [UsedImplicitly] - public sealed class AtmosphereSystem : SharedAtmosphereSystem + public override void Initialize() { + base.Initialize(); + SubscribeLocalEvent(OnMapHandleState); + } + + private void OnMapHandleState(EntityUid uid, MapAtmosphereComponent component, ref ComponentHandleState args) + { + if (args.Current is not MapAtmosphereComponentState state) + return; + + // Struct so should just copy by value. + component.OverlayData = state.Overlay; } } diff --git a/Content.Client/Atmos/Overlays/GasTileOverlay.cs b/Content.Client/Atmos/Overlays/GasTileOverlay.cs index 209b8ee6b0..d4e4e57d02 100644 --- a/Content.Client/Atmos/Overlays/GasTileOverlay.cs +++ b/Content.Client/Atmos/Overlays/GasTileOverlay.cs @@ -1,3 +1,4 @@ +using Content.Client.Atmos.Components; using Content.Client.Atmos.EntitySystems; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; @@ -7,6 +8,7 @@ using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Shared.Enums; using Robust.Shared.Map; +using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -136,76 +138,127 @@ namespace Content.Client.Atmos.Overlays protected override void Draw(in OverlayDrawArgs args) { + if (args.MapId == MapId.Nullspace) + return; + var drawHandle = args.WorldHandle; var xformQuery = _entManager.GetEntityQuery(); var overlayQuery = _entManager.GetEntityQuery(); + var gridState = (args.WorldBounds, + args.WorldHandle, + _gasCount, + _frames, + _frameCounter, + _fireFrames, + _fireFrameCounter, + _shader, + overlayQuery, + xformQuery); - foreach (var mapGrid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds)) + var mapUid = _mapManager.GetMapEntityId(args.MapId); + + if (_entManager.TryGetComponent(mapUid, out var atmos)) { - if (!overlayQuery.TryGetComponent(mapGrid.Owner, out var comp) || - !xformQuery.TryGetComponent(mapGrid.Owner, out var gridXform)) + var bottomLeft = args.WorldAABB.BottomLeft.Floored(); + var topRight = args.WorldAABB.TopRight.Ceiled(); + + for (var x = bottomLeft.X; x <= topRight.X; x++) { - continue; - } - - var (_, _, worldMatrix, invMatrix) = gridXform.GetWorldPositionRotationMatrixWithInv(); - drawHandle.SetTransform(worldMatrix); - var floatBounds = invMatrix.TransformBox(in args.WorldBounds).Enlarged(mapGrid.TileSize); - var localBounds = new Box2i( - (int) MathF.Floor(floatBounds.Left), - (int) MathF.Floor(floatBounds.Bottom), - (int) MathF.Ceiling(floatBounds.Right), - (int) MathF.Ceiling(floatBounds.Top)); - - // Currently it would be faster to group drawing by gas rather than by chunk, but if the textures are - // ever moved to a single atlas, that should no longer be the case. So this is just grouping draw calls - // by chunk, even though its currently slower. - - drawHandle.UseShader(null); - foreach (var chunk in comp.Chunks.Values) - { - var enumerator = new GasChunkEnumerator(chunk); - - while (enumerator.MoveNext(out var gas)) + for (var y = bottomLeft.Y; y <= topRight.Y; y++) { - if (gas.Opacity == null!) - continue; + var tilePosition = new Vector2(x, y); - var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y); - if (!localBounds.Contains(tilePosition)) - continue; - - for (var i = 0; i < _gasCount; i++) + for (var i = 0; i < atmos.OverlayData.Opacity.Length; i++) { - var opacity = gas.Opacity[i]; + var opacity = atmos.OverlayData.Opacity[i]; + if (opacity > 0) - drawHandle.DrawTexture(_frames[i][_frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity)); + args.WorldHandle.DrawTexture(_frames[i][_frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity)); } } } - - // And again for fire, with the unshaded shader - drawHandle.UseShader(_shader); - foreach (var chunk in comp.Chunks.Values) - { - var enumerator = new GasChunkEnumerator(chunk); - - while (enumerator.MoveNext(out var gas)) - { - if (gas.FireState == 0) - continue; - - var index = chunk.Origin + (enumerator.X, enumerator.Y); - if (!localBounds.Contains(index)) - continue; - - var state = gas.FireState - 1; - var texture = _fireFrames[state][_fireFrameCounter[state]]; - drawHandle.DrawTexture(texture, index); - } - } } + // TODO: WorldBounds callback. + _mapManager.FindGridsIntersecting(args.MapId, args.WorldAABB, ref gridState, + static (EntityUid uid, MapGridComponent grid, + ref (Box2Rotated WorldBounds, + DrawingHandleWorld drawHandle, + int gasCount, + Texture[][] frames, + int[] frameCounter, + Texture[][] fireFrames, + int[] fireFrameCounter, + ShaderInstance shader, + EntityQuery overlayQuery, + EntityQuery xformQuery) state) => + { + if (!state.overlayQuery.TryGetComponent(uid, out var comp) || + !state.xformQuery.TryGetComponent(uid, out var gridXform)) + { + return true; + } + + var (_, _, worldMatrix, invMatrix) = gridXform.GetWorldPositionRotationMatrixWithInv(); + state.drawHandle.SetTransform(worldMatrix); + var floatBounds = invMatrix.TransformBox(in state.WorldBounds).Enlarged(grid.TileSize); + var localBounds = new Box2i( + (int) MathF.Floor(floatBounds.Left), + (int) MathF.Floor(floatBounds.Bottom), + (int) MathF.Ceiling(floatBounds.Right), + (int) MathF.Ceiling(floatBounds.Top)); + + // Currently it would be faster to group drawing by gas rather than by chunk, but if the textures are + // ever moved to a single atlas, that should no longer be the case. So this is just grouping draw calls + // by chunk, even though its currently slower. + + state.drawHandle.UseShader(null); + foreach (var chunk in comp.Chunks.Values) + { + var enumerator = new GasChunkEnumerator(chunk); + + while (enumerator.MoveNext(out var gas)) + { + if (gas.Opacity == null!) + continue; + + var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y); + if (!localBounds.Contains(tilePosition)) + continue; + + for (var i = 0; i < state.gasCount; i++) + { + var opacity = gas.Opacity[i]; + if (opacity > 0) + state.drawHandle.DrawTexture(state.frames[i][state.frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity)); + } + } + } + + // And again for fire, with the unshaded shader + state.drawHandle.UseShader(state.shader); + foreach (var chunk in comp.Chunks.Values) + { + var enumerator = new GasChunkEnumerator(chunk); + + while (enumerator.MoveNext(out var gas)) + { + if (gas.FireState == 0) + continue; + + var index = chunk.Origin + (enumerator.X, enumerator.Y); + if (!localBounds.Contains(index)) + continue; + + var fireState = gas.FireState - 1; + var texture = state.fireFrames[fireState][state.fireFrameCounter[fireState]]; + state.drawHandle.DrawTexture(texture, index); + } + } + + return true; + }); + drawHandle.UseShader(null); drawHandle.SetTransform(Matrix3.Identity); } diff --git a/Content.Client/Parallax/ParallaxOverlay.cs b/Content.Client/Parallax/ParallaxOverlay.cs index 642cdec896..59fc8fce51 100644 --- a/Content.Client/Parallax/ParallaxOverlay.cs +++ b/Content.Client/Parallax/ParallaxOverlay.cs @@ -46,7 +46,7 @@ public sealed class ParallaxOverlay : Overlay return; var position = args.Viewport.Eye?.Position.Position ?? Vector2.Zero; - var screenHandle = args.WorldHandle; + var worldHandle = args.WorldHandle; var layers = _parallax.GetParallaxLayers(args.MapId); var realTime = (float) _timing.RealTime.TotalSeconds; @@ -60,7 +60,7 @@ public sealed class ParallaxOverlay : Overlay else shader = null; - screenHandle.UseShader(shader); + worldHandle.UseShader(shader); var tex = layer.Texture; // Size of the texture in world units. @@ -101,17 +101,17 @@ public sealed class ParallaxOverlay : Overlay { for (var y = flooredBL.Y; y < args.WorldAABB.Top; y += size.Y) { - screenHandle.DrawTextureRect(tex, Box2.FromDimensions((x, y), size)); + worldHandle.DrawTextureRect(tex, Box2.FromDimensions((x, y), size)); } } } else { - screenHandle.DrawTextureRect(tex, Box2.FromDimensions(originBL, size)); + worldHandle.DrawTextureRect(tex, Box2.FromDimensions(originBL, size)); } } - screenHandle.UseShader(null); + worldHandle.UseShader(null); } } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index 38e2937826..8e6ff2035e 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -10,6 +10,7 @@ using Content.Client.Chemistry.UI; using Content.Client.Construction; using Content.Server.Atmos; using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; using Content.Server.Construction.Components; using Content.Server.Gravity; using Content.Server.Power.Components; @@ -927,17 +928,16 @@ public abstract partial class InteractionTest var target = uid ?? MapData.MapUid; await Server.WaitPost(() => { + var atmosSystem = SEntMan.System(); var atmos = SEntMan.EnsureComponent(target); - atmos.Space = false; var moles = new float[Atmospherics.AdjustedNumberOfGases]; moles[(int) Gas.Oxygen] = 21.824779f; moles[(int) Gas.Nitrogen] = 82.10312f; - - atmos.Mixture = new GasMixture(2500) + atmosSystem.SetMapAtmosphere(target, false, new GasMixture(2500) { Temperature = 293.15f, Moles = moles, - }; + }, atmos); }); } diff --git a/Content.Server/Atmos/Components/MapAtmosphereComponent.cs b/Content.Server/Atmos/Components/MapAtmosphereComponent.cs index 09a2c34833..798dd6cba0 100644 --- a/Content.Server/Atmos/Components/MapAtmosphereComponent.cs +++ b/Content.Server/Atmos/Components/MapAtmosphereComponent.cs @@ -1,11 +1,13 @@ +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; + namespace Content.Server.Atmos.Components; /// /// Component that defines the default GasMixture for a map. /// -/// Honestly, no need to [Friend] this. It's just two simple data fields... Change them to your heart's content. -[RegisterComponent] -public sealed class MapAtmosphereComponent : Component +[RegisterComponent, Access(typeof(SharedAtmosphereSystem))] +public sealed class MapAtmosphereComponent : SharedMapAtmosphereComponent { /// /// The default GasMixture a map will have. Space mixture by default. diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Map.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Map.cs index ecb784c763..916191cb05 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Map.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Map.cs @@ -1,4 +1,6 @@ using Content.Server.Atmos.Components; +using Content.Shared.Atmos.Components; +using Robust.Shared.GameStates; namespace Content.Server.Atmos.EntitySystems; @@ -9,6 +11,7 @@ public partial class AtmosphereSystem SubscribeLocalEvent(MapIsTileSpace); SubscribeLocalEvent(MapGetTileMixture); SubscribeLocalEvent(MapGetTileMixtures); + SubscribeLocalEvent(OnMapGetState); } private void MapIsTileSpace(EntityUid uid, MapAtmosphereComponent component, ref IsTileSpaceMethodEvent args) @@ -42,4 +45,37 @@ public partial class AtmosphereSystem args.Mixtures[i] ??= component.Mixture.Clone(); } } + + private void OnMapGetState(EntityUid uid, MapAtmosphereComponent component, ref ComponentGetState args) + { + args.State = new MapAtmosphereComponentState(_gasTileOverlaySystem.GetOverlayData(component.Mixture)); + } + + public void SetMapAtmosphere(EntityUid uid, bool space, GasMixture mixture, MapAtmosphereComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.Space = space; + component.Mixture = mixture; + Dirty(component); + } + + public void SetMapGasMixture(EntityUid uid, GasMixture? mixture, MapAtmosphereComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.Mixture = mixture; + Dirty(component); + } + + public void SetMapSpace(EntityUid uid, bool space, MapAtmosphereComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.Space = space; + Dirty(component); + } } diff --git a/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs index 70117a4e40..836d3ba9bf 100644 --- a/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs @@ -139,6 +139,39 @@ namespace Content.Server.Atmos.EntitySystems } } + private byte GetOpacity(float moles, float molesVisible, float molesVisibleMax) + { + return (byte) (ContentHelpers.RoundToLevels( + MathHelper.Clamp01((moles - molesVisible) / + (molesVisibleMax - molesVisible)) * 255, byte.MaxValue, + _thresholds) * 255 / (_thresholds - 1)); + } + + public GasOverlayData GetOverlayData(GasMixture? mixture) + { + var data = new GasOverlayData(0, new byte[VisibleGasId.Length]); + + for (var i = 0; i < VisibleGasId.Length; i++) + { + var id = VisibleGasId[i]; + var gas = _atmosphereSystem.GetGas(id); + var moles = mixture?.Moles[id] ?? 0f; + ref var opacity = ref data.Opacity[i]; + + if (moles < gas.GasMolesVisible) + { + continue; + } + + opacity = (byte) (ContentHelpers.RoundToLevels( + MathHelper.Clamp01((moles - gas.GasMolesVisible) / + (gas.GasMolesVisibleMax - gas.GasMolesVisible)) * 255, byte.MaxValue, + _thresholds) * 255 / (_thresholds - 1)); + } + + return data; + } + /// /// Updates the visuals for a tile on some grid chunk. Returns true if the visuals have changed. /// @@ -187,10 +220,7 @@ namespace Content.Server.Atmos.EntitySystems continue; } - var opacity = (byte) (ContentHelpers.RoundToLevels( - MathHelper.Clamp01((moles - gas.GasMolesVisible) / - (gas.GasMolesVisibleMax - gas.GasMolesVisible)) * 255, byte.MaxValue, - _thresholds) * 255 / (_thresholds - 1)); + var opacity = GetOpacity(moles, gas.GasMolesVisible, gas.GasMolesVisibleMax); if (oldOpacity == opacity) continue; diff --git a/Content.Server/Maps/PlanetCommand.cs b/Content.Server/Maps/PlanetCommand.cs index 902d25388b..ed69124d33 100644 --- a/Content.Server/Maps/PlanetCommand.cs +++ b/Content.Server/Maps/PlanetCommand.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Server.Administration; using Content.Server.Atmos; using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; using Content.Server.Parallax; using Content.Shared.Administration; using Content.Shared.Atmos; @@ -85,17 +86,18 @@ public sealed class PlanetCommand : IConsoleCommand // Atmos var atmos = _entManager.EnsureComponent(mapUid); - atmos.Space = false; var moles = new float[Atmospherics.AdjustedNumberOfGases]; moles[(int) Gas.Oxygen] = 21.824779f; moles[(int) Gas.Nitrogen] = 82.10312f; - atmos.Mixture = new GasMixture(2500) + var mixture = new GasMixture(2500) { Temperature = 293.15f, Moles = moles, }; + _entManager.System().SetMapAtmosphere(mapUid, false, mixture, atmos); + _entManager.EnsureComponent(mapUid); shell.WriteLine(Loc.GetString("cmd-planet-success", ("mapId", mapId))); } diff --git a/Content.Server/Salvage/SpawnSalvageMissionJob.cs b/Content.Server/Salvage/SpawnSalvageMissionJob.cs index 53ab835665..48e721c3d2 100644 --- a/Content.Server/Salvage/SpawnSalvageMissionJob.cs +++ b/Content.Server/Salvage/SpawnSalvageMissionJob.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Content.Server.Atmos; using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; using Robust.Shared.CPUJob.JobQueues; using Content.Server.Ghost.Roles.Components; using Content.Server.Parallax; @@ -111,13 +112,13 @@ public sealed class SpawnSalvageMissionJob : Job var moles = new float[Atmospherics.AdjustedNumberOfGases]; air.Gases.CopyTo(moles, 0); var atmos = _entManager.EnsureComponent(mapUid); - atmos.Space = air.Space; - atmos.Mixture = new GasMixture(2500) + _entManager.System().SetMapSpace(mapUid, air.Space, atmos); + _entManager.System().SetMapGasMixture(mapUid, new GasMixture(2500) { // TODO: temperature mods Temperature = 293.15f, Moles = moles, - }; + }, atmos); if (mission.Color != null) { diff --git a/Content.Shared/Atmos/Components/SharedMapAtmosphereComponent.cs b/Content.Shared/Atmos/Components/SharedMapAtmosphereComponent.cs new file mode 100644 index 0000000000..5d350a030a --- /dev/null +++ b/Content.Shared/Atmos/Components/SharedMapAtmosphereComponent.cs @@ -0,0 +1,22 @@ +using Content.Shared.Atmos.EntitySystems; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Atmos.Components; + +[NetworkedComponent] +public abstract class SharedMapAtmosphereComponent : Component +{ + [ViewVariables] public SharedGasTileOverlaySystem.GasOverlayData OverlayData; +} + +[Serializable, NetSerializable] +public sealed class MapAtmosphereComponentState : ComponentState +{ + public SharedGasTileOverlaySystem.GasOverlayData Overlay; + + public MapAtmosphereComponentState(SharedGasTileOverlaySystem.GasOverlayData overlay) + { + Overlay = overlay; + } +}