From 58af9003e7b6076d52a33ba879f53ebf77401d30 Mon Sep 17 00:00:00 2001 From: 20kdc Date: Tue, 8 Dec 2020 19:45:24 +0000 Subject: [PATCH] Canisters [ Continuation of clement-or #2544 ] (#2628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added atmos sprites from CEV-Eris * Moved canister sprites to appropriate dir * Removed unnecessary sprites, edited canisters prototype * Created Gas Canister UI and release pressure buttons * Changed GasMixture's pressure calculation (convert liters to cube meters) * Added relabeling Canisters * Reverted changes on GasMixture * Changed my name in the credits * Added valve opening on canisters * Change canister visual state when connected to a port * Added nullable to SharedGasCanisterComponent * Replaced nullable contexts * Changed again nullable annotation context * Moved name in the credits to correct alphabetical order * Canisters: Fix the most blatant issues with this PR (the added project interdependencies for no reason whatsoever) * Canisters: Stop crashes when canisters leave atmosphere * Canisters: Gas released into no atmosphere gets transferred "into space" (deleted) * Atmos: Nullability annotations on TileAtmosphere, explaination of the states of TileAtmosphere.Air * Canisters: If in an airblocked tile, do NOT release gas * Scrubbers: Fix typo leading to them not connecting properly. * Revert manual changes to credits file (sorry!) (1/2) This reverts commit 94f3b0e5df8d9c2fa189866a17a231920f99bdaf. * Revert manual changes to credits file (sorry!) (2/2) This reverts commit 1986fb094dfaa44060f08d280f36b755258d17a6. * Canisters: Apply @Zumorica 's reviews * Canisters: Add missing localization as suggested by PJB Co-authored-by: Pieter-Jan Briers * Canisters: Pressure lights! * Canisters: Light is now unshaded. * Canisters: Now using IActivate * Gas canisters (& air canister), now with their numbers properly calibrated (hopefully) * Canisters: Refactor how their layers are added to be more like ApcVisualizer * Canisters: Clean up of the tile invalidation/air release logic * Canisters: Some gas canister window improvements * Canisters: Clean up release pressure change button label code Co-authored-by: Clement-O Co-authored-by: Clément Co-authored-by: Pieter-Jan Briers --- .../Atmos/GasCanisterBoundUserInterface.cs | 115 ++++++++ .../Components/Atmos/GasCanisterVisualizer.cs | 72 +++++ .../Components/Atmos/GasCanisterWindow.cs | 166 +++++++++++ Content.Server/Atmos/GasMixture.cs | 9 +- Content.Server/Atmos/TileAtmosphere.cs | 11 +- .../Components/Atmos/GasCanisterComponent.cs | 257 +++++++++++++++++- .../Atmos/GridAtmosphereComponent.cs | 3 +- .../Atmos/Piping/GasCanisterPortComponent.cs | 1 + .../Disposal/DisposalUnitComponent.cs | 1 - .../Components/Doors/ServerDoorComponent.cs | 3 +- .../EntitySystems/GasCanisterSystem.cs | 19 ++ .../Atmos/SharedGasCanisterComponent.cs | 123 +++++++++ .../Constructible/Ground/gascanisters.yml | 93 ++++++- .../Constructible/Ground/scrubbers.yml | 2 +- .../Atmos/canister.rsi/black-1.png | Bin 0 -> 816 bytes .../Atmos/canister.rsi/black.png | Bin 0 -> 811 bytes .../Atmos/canister.rsi/blue-1.png | Bin 0 -> 909 bytes .../Constructible/Atmos/canister.rsi/blue.png | Bin 0 -> 952 bytes .../Atmos/canister.rsi/can-connector.png | Bin 0 -> 240 bytes .../Atmos/canister.rsi/can-o0.png | Bin 0 -> 135 bytes .../Atmos/canister.rsi/can-o1.png | Bin 0 -> 131 bytes .../Atmos/canister.rsi/can-o2.png | Bin 0 -> 140 bytes .../Atmos/canister.rsi/can-o3.png | Bin 0 -> 139 bytes .../Atmos/canister.rsi/can-oa1.png | Bin 0 -> 186 bytes .../Atmos/canister.rsi/can-open.png | Bin 0 -> 164 bytes .../Atmos/canister.rsi/grey-1.png | Bin 0 -> 860 bytes .../Constructible/Atmos/canister.rsi/grey.png | Bin 0 -> 864 bytes .../Atmos/canister.rsi/meta.json | 201 ++++++++++++++ .../Atmos/canister.rsi/orange-1.png | Bin 0 -> 839 bytes .../Atmos/canister.rsi/orange.png | Bin 0 -> 838 bytes .../Atmos/canister.rsi/red-1.png | Bin 0 -> 766 bytes .../Constructible/Atmos/canister.rsi/red.png | Bin 0 -> 765 bytes .../Atmos/canister.rsi/redws-1.png | Bin 0 -> 905 bytes .../Atmos/canister.rsi/redws.png | Bin 0 -> 957 bytes .../Atmos/canister.rsi/scrubber-connector.png | Bin 0 -> 218 bytes .../Atmos/canister.rsi/scrubber-open.png | Bin 0 -> 138 bytes .../Atmos/canister.rsi/yellow-1.png | Bin 0 -> 861 bytes .../Atmos/canister.rsi/yellow.png | Bin 0 -> 844 bytes 38 files changed, 1055 insertions(+), 21 deletions(-) create mode 100644 Content.Client/GameObjects/Components/Atmos/GasCanisterBoundUserInterface.cs create mode 100644 Content.Client/GameObjects/Components/Atmos/GasCanisterVisualizer.cs create mode 100644 Content.Client/GameObjects/Components/Atmos/GasCanisterWindow.cs create mode 100644 Content.Server/GameObjects/EntitySystems/GasCanisterSystem.cs create mode 100644 Content.Shared/GameObjects/Components/Atmos/SharedGasCanisterComponent.cs create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/black-1.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/black.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/blue-1.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/blue.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/can-connector.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/can-o0.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/can-o1.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/can-o2.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/can-o3.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/can-oa1.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/can-open.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/grey-1.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/grey.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/meta.json create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/orange-1.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/orange.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/red-1.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/red.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/redws-1.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/redws.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/scrubber-connector.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/scrubber-open.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/yellow-1.png create mode 100644 Resources/Textures/Constructible/Atmos/canister.rsi/yellow.png diff --git a/Content.Client/GameObjects/Components/Atmos/GasCanisterBoundUserInterface.cs b/Content.Client/GameObjects/Components/Atmos/GasCanisterBoundUserInterface.cs new file mode 100644 index 0000000000..d6aa8a80c2 --- /dev/null +++ b/Content.Client/GameObjects/Components/Atmos/GasCanisterBoundUserInterface.cs @@ -0,0 +1,115 @@ +#nullable enable +using System; +using System.Diagnostics; +using JetBrains.Annotations; +using Robust.Client.GameObjects.Components.UserInterface; +using Robust.Shared.GameObjects.Components.UserInterface; +using Content.Client.GameObjects.Components.Atmos; +using Content.Shared.GameObjects.Components.Atmos; +using Robust.Shared.Localization; + +namespace Content.Client.GameObjects.Components.Atmos +{ + /// + /// Initializes a and updates it when new server messages are received. + /// + [UsedImplicitly] + public class GasCanisterBoundUserInterface : BoundUserInterface + { + + private GasCanisterWindow? _window; + + public GasCanisterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + + } + + + /// + /// When a button is pressed, send a network message to the server + /// + /// Which button has been pressed, as an enum item + private void ButtonPressed(UiButton button) + { + SendMessage(new UiButtonPressedMessage(button)); + } + + + /// + /// When the release pressure is changed + /// + /// The pressure value + private void ReleasePressureButtonPressed(float value) + { + SendMessage(new ReleasePressureButtonPressedMessage(value)); + } + + + protected override void Open() + { + base.Open(); + + _window = new GasCanisterWindow(); + _window.Title = Loc.GetString("Gas Canister"); + + _window.OpenCentered(); + _window.OnClose += Close; + + // Bind buttons OnPressed event + foreach (ReleasePressureButton btn in _window.ReleasePressureButtons) + { + btn.OnPressed += _ => ReleasePressureButtonPressed(btn.PressureChange); + } + + // Bind events + _window.EditLabelBtn.OnPressed += _ => EditLabel(); + _window.ToggleValve.OnPressed += _ => ToggleValve(); + } + + + /// + /// Called when the edit label button is pressed + /// + private void EditLabel() + { + // Obligatory check because bool isn't nullable + if (_window == null) return; + + if (_window.LabelInputEditable) + { + if (_window.LabelInput.Text != _window.OldLabel) + SendMessage(new CanisterLabelChangedMessage(_window.LabelInput.Text)); + + _window.LabelInputEditable = false; + } + else + { + _window.LabelInputEditable = true; + _window.LabelInput.HasKeyboardFocus(); + } + } + + + private void ToggleValve() + { + SendMessage(new UiButtonPressedMessage(UiButton.ValveToggle)); + } + + + /// + /// Update the UI state based on server-sent info + /// + /// + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (!(state is GasCanisterBoundUserInterfaceState cast)) + { + return; + } + + _window?.UpdateState(cast); + } + } +} diff --git a/Content.Client/GameObjects/Components/Atmos/GasCanisterVisualizer.cs b/Content.Client/GameObjects/Components/Atmos/GasCanisterVisualizer.cs new file mode 100644 index 0000000000..45cded0ab0 --- /dev/null +++ b/Content.Client/GameObjects/Components/Atmos/GasCanisterVisualizer.cs @@ -0,0 +1,72 @@ +using Content.Shared.GameObjects.Components.Atmos; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Interfaces.GameObjects.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Maths; +using Robust.Shared.Utility; +using YamlDotNet.RepresentationModel; + +namespace Content.Client.GameObjects.Components.Atmos +{ + public class GasCanisterVisualizer : AppearanceVisualizer + { + private string _stateConnected; + private string[] _statePressure = new string[] {"", "", "", ""}; + + public override void LoadData(YamlMappingNode node) + { + base.LoadData(node); + + _stateConnected = node.GetNode("stateConnected").AsString(); + for (int i = 0; i < _statePressure.Length; i++) + _statePressure[i] = node.GetNode("stateO" + i).AsString(); + } + + public override void InitializeEntity(IEntity entity) + { + base.InitializeEntity(entity); + + var sprite = entity.GetComponent(); + + sprite.LayerMapSet(Layers.ConnectedToPort, sprite.AddLayerState(_stateConnected)); + sprite.LayerSetVisible(Layers.ConnectedToPort, false); + + sprite.LayerMapSet(Layers.PressureLight, sprite.AddLayerState(_stateConnected)); + sprite.LayerSetShader(Layers.PressureLight, "unshaded"); + } + + public override void OnChangeData(AppearanceComponent component) + { + base.OnChangeData(component); + + if (component.Deleted) + { + return; + } + + if (!component.Owner.TryGetComponent(out ISpriteComponent sprite)) + { + return; + } + + // Update the visuals : Is the canister connected to a port or not + if (component.TryGetData(GasCanisterVisuals.ConnectedState, out bool isConnected)) + { + sprite.LayerSetVisible(Layers.ConnectedToPort, isConnected); + } + + // Update the visuals : Canister lights + if (component.TryGetData(GasCanisterVisuals.PressureState, out int pressureState)) + if ((pressureState >= 0) && (pressureState < _statePressure.Length)) + sprite.LayerSetState(Layers.PressureLight, _statePressure[pressureState]); + } + + enum Layers + { + ConnectedToPort, + PressureLight + } + } +} diff --git a/Content.Client/GameObjects/Components/Atmos/GasCanisterWindow.cs b/Content.Client/GameObjects/Components/Atmos/GasCanisterWindow.cs new file mode 100644 index 0000000000..96334c6232 --- /dev/null +++ b/Content.Client/GameObjects/Components/Atmos/GasCanisterWindow.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Shared.GameObjects.Components.Disposal; +using Robust.Client.Graphics.Drawing; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Content.Client.GameObjects.Components.Atmos; +using Content.Shared.GameObjects.Components.Atmos; + +namespace Content.Client.GameObjects.Components.Atmos +{ + /// + /// Client-side UI used to control a + /// + public class GasCanisterWindow : SS14Window + { + private readonly Label _pressure; + private readonly Label _releasePressure; + + public readonly CheckButton ToggleValve; + public readonly LineEdit LabelInput; + public readonly Button EditLabelBtn; + public string OldLabel { get; set; } = ""; + + public bool LabelInputEditable { + get => LabelInput.Editable; + set { + LabelInput.Editable = value; + EditLabelBtn.Text = value ? Loc.GetString("OK") : Loc.GetString("Edit"); + } + } + + public List ReleasePressureButtons { get; private set; } + + protected override Vector2? CustomSize => (300, 200); + + public GasCanisterWindow() + { + HBoxContainer releasePressureButtons; + + Contents.AddChild(new VBoxContainer + { + Children = + { + new VBoxContainer + { + Children = + { + new HBoxContainer() + { + Children = + { + new Label(){ Text = Loc.GetString("Label") }, + (LabelInput = new LineEdit() { Text = Name, Editable = false, + CustomMinimumSize = new Vector2(200, 30)}), + (EditLabelBtn = new Button()), + } + }, + new HBoxContainer + { + Children = + { + new Label {Text = Loc.GetString("Pressure:")}, + (_pressure = new Label()) + } + }, + new VBoxContainer() + { + Children = + { + new HBoxContainer() + { + Children = + { + new Label() {Text = Loc.GetString("Release pressure:")}, + (_releasePressure = new Label()) + } + }, + (releasePressureButtons = new HBoxContainer() + { + Children = + { + new ReleasePressureButton() {PressureChange = -50}, + new ReleasePressureButton() {PressureChange = -10}, + new ReleasePressureButton() {PressureChange = -1}, + new ReleasePressureButton() {PressureChange = -0.1f}, + new ReleasePressureButton() {PressureChange = 0.1f}, + new ReleasePressureButton() {PressureChange = 1}, + new ReleasePressureButton() {PressureChange = 10}, + new ReleasePressureButton() {PressureChange = 50} + } + }) + } + }, + new HBoxContainer() + { + Children = + { + new Label { Text = Loc.GetString("Valve") }, + (ToggleValve = new CheckButton() { Text = Loc.GetString("Open") }) + } + } + }, + } + } + }); + + // Create the release pressure buttons list + ReleasePressureButtons = new List(); + foreach (var control in releasePressureButtons.Children.ToList()) + { + var btn = (ReleasePressureButton) control; + ReleasePressureButtons.Add(btn); + } + + // Reset the editable label + LabelInputEditable = false; + } + + + /// + /// Update the UI based on + /// + /// The state the UI should reflect + public void UpdateState(GasCanisterBoundUserInterfaceState state) + { + _pressure.Text = Loc.GetString("{0}kPa", state.Volume); + _releasePressure.Text = Loc.GetString("{0}kPa", state.ReleasePressure); + + // Update the canister label + OldLabel = LabelInput.Text; + LabelInput.Text = state.Label; + Title = state.Label; + + // Reset the editable label + LabelInputEditable = false; + + ToggleValve.Pressed = state.ValveOpened; + } + } + + + /// + /// Special button class which stores a numerical value and has it as a label + /// + public class ReleasePressureButton : Button + { + public float PressureChange + { + get { return _pressureChange; } + set + { + _pressureChange = value; + Text = (value >= 0) ? ("+" + value) : value.ToString(); + } + } + + private float _pressureChange; + + public ReleasePressureButton() : base() {} + } +} diff --git a/Content.Server/Atmos/GasMixture.cs b/Content.Server/Atmos/GasMixture.cs index bb21c9a1e2..108326bcf6 100644 --- a/Content.Server/Atmos/GasMixture.cs +++ b/Content.Server/Atmos/GasMixture.cs @@ -452,15 +452,16 @@ namespace Content.Server.Atmos /// /// Releases gas from this mixture to the output mixture. + /// If the output mixture is null, then this is being released into space. /// It can't transfer air to a mixture with higher pressure. /// /// /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ReleaseGasTo(GasMixture outputAir, float targetPressure) + public bool ReleaseGasTo(GasMixture? outputAir, float targetPressure) { - var outputStartingPressure = outputAir.Pressure; + var outputStartingPressure = outputAir?.Pressure ?? 0; var inputStartingPressure = Pressure; if (outputStartingPressure >= MathF.Min(targetPressure, inputStartingPressure - 10)) @@ -472,11 +473,11 @@ namespace Content.Server.Atmos // We calculate the necessary moles to transfer with the ideal gas law. var pressureDelta = MathF.Min(targetPressure - outputStartingPressure, (inputStartingPressure - outputStartingPressure) / 2f); - var transferMoles = pressureDelta * outputAir.Volume / (Temperature * Atmospherics.R); + var transferMoles = pressureDelta * (outputAir?.Volume ?? Atmospherics.CellVolume) / (Temperature * Atmospherics.R); // And now we transfer the gas. var removed = Remove(transferMoles); - outputAir.Merge(removed); + outputAir?.Merge(removed); return true; diff --git a/Content.Server/Atmos/TileAtmosphere.cs b/Content.Server/Atmos/TileAtmosphere.cs index 749a441d61..8cd4f43f60 100644 --- a/Content.Server/Atmos/TileAtmosphere.cs +++ b/Content.Server/Atmos/TileAtmosphere.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable annotations +using System; using System.Buffers; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -49,7 +50,7 @@ namespace Content.Server.Atmos private static int _soundCooldown; [ViewVariables] - public TileAtmosphere PressureSpecificTarget { get; set; } + public TileAtmosphere? PressureSpecificTarget { get; set; } [ViewVariables] public float PressureDifference { get; set; } @@ -103,8 +104,12 @@ namespace Content.Server.Atmos [ViewVariables] public ExcitedGroup ExcitedGroup { get; set; } + /// + /// The air in this tile. If null, this tile is completely airblocked. + /// This can be immutable if the tile is spaced. + /// [ViewVariables] - public GasMixture Air { get; set; } + public GasMixture? Air { get; set; } [ViewVariables, UsedImplicitly] private int _blockedAirflow => (int)BlockedAirflow; diff --git a/Content.Server/GameObjects/Components/Atmos/GasCanisterComponent.cs b/Content.Server/GameObjects/Components/Atmos/GasCanisterComponent.cs index bb874d5b33..3ad88356e0 100644 --- a/Content.Server/GameObjects/Components/Atmos/GasCanisterComponent.cs +++ b/Content.Server/GameObjects/Components/Atmos/GasCanisterComponent.cs @@ -1,4 +1,6 @@ -using Content.Server.Atmos; +#nullable enable +using System; +using Content.Server.Atmos; using Content.Server.GameObjects.Components.Atmos.Piping; using Content.Server.Interfaces; using Robust.Shared.GameObjects; @@ -6,36 +8,83 @@ using Robust.Shared.GameObjects.Components; using Robust.Shared.GameObjects.Components.Transform; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; -using System; using System.Linq; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Interfaces.GameObjects.Components.Items; +using Content.Server.Utility; +using Content.Shared.GameObjects.Components.Atmos; +using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Content.Shared.Atmos; +using Robust.Server.GameObjects; +using Robust.Server.GameObjects.Components.UserInterface; +using Robust.Server.Interfaces.GameObjects; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Localization; namespace Content.Server.GameObjects.Components.Atmos { + /// + /// Component that manages gas mixtures temperature, pressure and output. + /// [RegisterComponent] - public class GasCanisterComponent : Component, IGasMixtureHolder + [ComponentReference(typeof(IActivate))] + public class GasCanisterComponent : Component, IGasMixtureHolder, IActivate { public override string Name => "GasCanister"; - [ViewVariables] - public GasMixture Air { get; set; } + private const int MaxLabelLength = 32; + + [ViewVariables(VVAccess.ReadWrite)] + public string Label { get; set; } = "Gas Canister"; + + [ViewVariables(VVAccess.ReadWrite)] + public bool ValveOpened { get; set; } = false; + + /// + /// What the canister contains. + /// + [ViewVariables(VVAccess.ReadWrite)] + public GasMixture Air { get; set; } = default!; [ViewVariables] public bool Anchored => !Owner.TryGetComponent(out var physics) || physics.Anchored; + /// + /// The floor connector port that the canister is attached to. + /// [ViewVariables] - public GasCanisterPortComponent ConnectedPort { get; private set; } + public GasCanisterPortComponent? ConnectedPort { get; private set; } [ViewVariables] public bool ConnectedToPort => ConnectedPort != null; private const float DefaultVolume = 10; + [ViewVariables(VVAccess.ReadWrite)] public float ReleasePressure { get; set; } + + /// + /// The user interface bound to the canister. + /// + private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SharedGasCanisterComponent.GasCanisterUiKey.Key); + + /// + /// Stores the last ui state after it's been casted into + /// + private GasCanisterBoundUserInterfaceState? _lastUiState; + + private AppearanceComponent? _appearance; + public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); serializer.DataField(this, x => Air, "gasMixture", new GasMixture(DefaultVolume)); } + public override void Initialize() { base.Initialize(); @@ -44,8 +93,21 @@ namespace Content.Server.GameObjects.Components.Atmos AnchorUpdate(); physics.AnchoredChanged += AnchorUpdate; } + if (UserInterface != null) + { + UserInterface.OnReceiveMessage += OnUiReceiveMessage; + } + + // Init some variables + Label = Owner.Name; + Owner.TryGetComponent(out _appearance); + + UpdateUserInterface(); + UpdateAppearance(); } + #region Connector port methods + public override void OnRemove() { base.OnRemove(); @@ -53,23 +115,27 @@ namespace Content.Server.GameObjects.Components.Atmos { physics.AnchoredChanged -= AnchorUpdate; } + if (UserInterface != null) + { + UserInterface.OnReceiveMessage -= OnUiReceiveMessage; + } DisconnectFromPort(); } - public void TryConnectToPort() { if (!Owner.TryGetComponent(out var snapGrid)) return; var port = snapGrid.GetLocal() .Select(entity => entity.TryGetComponent(out var port) ? port : null) .Where(port => port != null) - .Where(port => !port.ConnectedToCanister) + .Where(port => !port!.ConnectedToCanister) .FirstOrDefault(); if (port == null) return; ConnectedPort = port; ConnectedPort.ConnectGasCanister(this); } + public void DisconnectFromPort() { ConnectedPort?.DisconnectGasCanister(); @@ -86,6 +152,181 @@ namespace Content.Server.GameObjects.Components.Atmos { DisconnectFromPort(); } + UpdateAppearance(); + } + + #endregion + + void IActivate.Activate(ActivateEventArgs eventArgs) + { + if (!eventArgs.User.TryGetComponent(out IActorComponent? actor)) + return; + + UserInterface?.Open(actor.playerSession); + } + + private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj) + { + if (obj.Session.AttachedEntity == null) + { + return; + } + + if (!PlayerCanUse(obj.Session.AttachedEntity)) + { + return; + } + + // If the label has been changed by a client + if (obj.Message is CanisterLabelChangedMessage canLabelMessage) + { + var newLabel = canLabelMessage.NewLabel; + if (newLabel.Length > MaxLabelLength) + newLabel = newLabel.Substring(0, MaxLabelLength); + Label = newLabel; + Owner.Name = Label; + UpdateUserInterface(); + return; + } + + // If the release pressure has been adjusted by the client on the gas canister + if (obj.Message is ReleasePressureButtonPressedMessage rPMessage) + { + ReleasePressure += rPMessage.ReleasePressure; + ReleasePressure = Math.Clamp(ReleasePressure, 0, 1000); + ReleasePressure = MathF.Round(ReleasePressure, 2); + UpdateUserInterface(); + return; + } + + + if (obj.Message is UiButtonPressedMessage btnPressedMessage) + { + switch (btnPressedMessage.Button) + { + case UiButton.ValveToggle: + ToggleValve(); + break; + } + } + } + + /// + /// Update the user interface if relevant + /// + private void UpdateUserInterface() + { + var state = GetUserInterfaceState(); + + if (_lastUiState != null && _lastUiState.Equals(state)) + { + return; + } + + _lastUiState = state; + UserInterface?.SetState(state); + } + + /// + /// Update the canister's sprite + /// + private void UpdateAppearance() + { + _appearance?.SetData(GasCanisterVisuals.ConnectedState, ConnectedToPort); + // The Eris canisters are being used, so best to use the Eris light logic unless someone else has a better idea. + // https://github.com/discordia-space/CEV-Eris/blob/fdd6ee7012f46838a6711adb1737cd90c48ae448/code/game/machinery/atmoalter/canister.dm#L129 + if (Air.Pressure < 10) + { + _appearance?.SetData(GasCanisterVisuals.PressureState, 0); + } + else if (Air.Pressure < Atmospherics.OneAtmosphere) + { + _appearance?.SetData(GasCanisterVisuals.PressureState, 1); + } + else if (Air.Pressure < (15 * Atmospherics.OneAtmosphere)) + { + _appearance?.SetData(GasCanisterVisuals.PressureState, 2); + } + else + { + _appearance?.SetData(GasCanisterVisuals.PressureState, 3); + } + } + + /// + /// Get the current interface state from server data + /// + /// The state + private GasCanisterBoundUserInterfaceState GetUserInterfaceState() + { + // We round the pressure for ease of reading + return new GasCanisterBoundUserInterfaceState(Label, + MathF.Round(Air.Pressure, 2), + ReleasePressure, + ValveOpened); + } + + public void AirWasUpdated() + { + UpdateUserInterface(); + UpdateAppearance(); + } + + #region Check methods + + private bool PlayerCanUse(IEntity? player) + { + if (player == null) + { + return false; + } + + if (!ActionBlockerSystem.CanInteract(player) || + !ActionBlockerSystem.CanUse(player)) + { + return false; + } + + return true; + } + + #endregion + + + /// + /// Called when the canister's valve is toggled + /// + private void ToggleValve() + { + ValveOpened = !ValveOpened; + UpdateUserInterface(); + } + + /// + /// Called every frame + /// + /// + public void Update(in float frameTime) + { + if (ValveOpened) + { + var tileAtmosphere = Owner.Transform.Coordinates.GetTileAtmosphere(); + if (tileAtmosphere != null) + { + // If tileAtmosphere.Air is null, then we're airblocked, so DON'T release + if (tileAtmosphere.Air != null) + { + Air.ReleaseGasTo(tileAtmosphere.Air, ReleasePressure); + tileAtmosphere.Invalidate(); + } + } + else + { + Air.ReleaseGasTo(null, ReleasePressure); + } + + AirWasUpdated(); + } } } } diff --git a/Content.Server/GameObjects/Components/Atmos/GridAtmosphereComponent.cs b/Content.Server/GameObjects/Components/Atmos/GridAtmosphereComponent.cs index 717e8afc17..0c03c2f292 100644 --- a/Content.Server/GameObjects/Components/Atmos/GridAtmosphereComponent.cs +++ b/Content.Server/GameObjects/Components/Atmos/GridAtmosphereComponent.cs @@ -283,6 +283,7 @@ namespace Content.Server.GameObjects.Components.Atmos { var tile = GetTile(indices); if (tile?.GridIndex != _gridId) return; + // includeAirBlocked is false, therefore all tiles in this have Air != null. var adjacent = GetAdjacentTiles(indices); tile.Air = new GasMixture(GetVolumeForCells(1), AtmosphereSystem){Temperature = Atmospherics.T20C}; Tiles[indices] = tile; @@ -291,7 +292,7 @@ namespace Content.Server.GameObjects.Components.Atmos foreach (var (_, adj) in adjacent) { - var mix = adj.Air.RemoveRatio(ratio); + var mix = adj.Air!.RemoveRatio(ratio); tile.Air.Merge(mix); adj.Air.Merge(mix); } diff --git a/Content.Server/GameObjects/Components/Atmos/Piping/GasCanisterPortComponent.cs b/Content.Server/GameObjects/Components/Atmos/Piping/GasCanisterPortComponent.cs index 3fe989f7a2..39e0289d47 100644 --- a/Content.Server/GameObjects/Components/Atmos/Piping/GasCanisterPortComponent.cs +++ b/Content.Server/GameObjects/Components/Atmos/Piping/GasCanisterPortComponent.cs @@ -62,6 +62,7 @@ namespace Content.Server.GameObjects.Components.Atmos.Piping public override void Update() { ConnectedCanister?.Air.Share(_gasPort.Air, 1); + ConnectedCanister?.AirWasUpdated(); } public void ConnectGasCanister(GasCanisterComponent gasCanister) diff --git a/Content.Server/GameObjects/Components/Disposal/DisposalUnitComponent.cs b/Content.Server/GameObjects/Components/Disposal/DisposalUnitComponent.cs index a77aae196f..31a1ac6a86 100644 --- a/Content.Server/GameObjects/Components/Disposal/DisposalUnitComponent.cs +++ b/Content.Server/GameObjects/Components/Disposal/DisposalUnitComponent.cs @@ -414,7 +414,6 @@ namespace Content.Server.GameObjects.Components.Disposal case UiButton.Power: TogglePower(); EntitySystem.Get().PlayFromEntity("/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f)); - break; default: throw new ArgumentOutOfRangeException(); diff --git a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs index 78f6f46b77..47949d6227 100644 --- a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs +++ b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs @@ -351,7 +351,8 @@ namespace Content.Server.GameObjects.Components.Doors foreach (var (_, adjacent) in gridAtmosphere.GetAdjacentTiles(tileAtmos.GridIndices)) { - var moles = adjacent.Air.TotalMoles; + // includeAirBlocked remains false, and therefore Air must be present + var moles = adjacent.Air!.TotalMoles; if (moles < minMoles) minMoles = moles; if (moles > maxMoles) diff --git a/Content.Server/GameObjects/EntitySystems/GasCanisterSystem.cs b/Content.Server/GameObjects/EntitySystems/GasCanisterSystem.cs new file mode 100644 index 0000000000..a9c74af4f7 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/GasCanisterSystem.cs @@ -0,0 +1,19 @@ +using Content.Server.GameObjects.Components.Atmos; +using Content.Server.GameObjects.Components.Recycling; +using JetBrains.Annotations; +using Robust.Shared.GameObjects.Systems; + +namespace Content.Server.GameObjects.EntitySystems +{ + [UsedImplicitly] + internal sealed class GasCanisterSystem : EntitySystem + { + public override void Update(float frameTime) + { + foreach (var component in ComponentManager.EntityQuery()) + { + component.Update(frameTime); + } + } + } +} diff --git a/Content.Shared/GameObjects/Components/Atmos/SharedGasCanisterComponent.cs b/Content.Shared/GameObjects/Components/Atmos/SharedGasCanisterComponent.cs new file mode 100644 index 0000000000..4d1f0ea6b0 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Atmos/SharedGasCanisterComponent.cs @@ -0,0 +1,123 @@ +#nullable enable annotations +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Shared.GameObjects.Components.Atmos +{ + public class SharedGasCanisterComponent : Component + { + public override string Name => "GasCanister"; + + /// + /// Key representing which is currently open. + /// Useful when there are multiple UI for an object. Here it's future-proofing only. + /// + [Serializable, NetSerializable] + public enum GasCanisterUiKey + { + Key, + } + } + + #region Enums + + /// + /// Enum representing a UI button. + /// + [Serializable, NetSerializable] + public enum UiButton + { + ValveToggle + } + + /// + /// Used in to determine which visuals to update. + /// + [Serializable, NetSerializable] + public enum GasCanisterVisuals + { + ConnectedState, + PressureState + } + + #endregion + + /// + /// Represents a state that can be sent to the client + /// + [Serializable, NetSerializable] + public class GasCanisterBoundUserInterfaceState : BoundUserInterfaceState + { + public readonly string Label; + public readonly float Volume; + public readonly float ReleasePressure; + public readonly bool ValveOpened; + + public GasCanisterBoundUserInterfaceState(string newLabel, float volume, float releasePressure, bool valveOpened) + { + Label = newLabel; + Volume = volume; + ReleasePressure = releasePressure; + ValveOpened = valveOpened; + } + + public bool Equals(GasCanisterBoundUserInterfaceState? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Label == other.Label && + Volume.Equals(other.Volume) && + ReleasePressure.Equals(other.ReleasePressure) && + ValveOpened == other.ValveOpened; + } + } + + #region NetMessages + + /// + /// Message sent from the client to the server when a gas canister button is pressed + /// + [Serializable, NetSerializable] + public class UiButtonPressedMessage : BoundUserInterfaceMessage + { + public readonly UiButton Button; + + public UiButtonPressedMessage(UiButton button) + { + Button = button; + } + } + + /// + /// Message sent when the release pressure is changed client side + /// + [Serializable, NetSerializable] + public class ReleasePressureButtonPressedMessage : BoundUserInterfaceMessage + { + public readonly float ReleasePressure; + + public ReleasePressureButtonPressedMessage(float val) : base() + { + ReleasePressure = val; + } + } + + /// + /// Message sent when the canister label has been changed + /// + [Serializable, NetSerializable] + public class CanisterLabelChangedMessage : BoundUserInterfaceMessage + { + public readonly string NewLabel; + + public CanisterLabelChangedMessage(string newLabel) : base() + { + NewLabel = newLabel; + } + } + + #endregion +} diff --git a/Resources/Prototypes/Entities/Constructible/Ground/gascanisters.yml b/Resources/Prototypes/Entities/Constructible/Ground/gascanisters.yml index 276cacbbf6..53b7a7b633 100644 --- a/Resources/Prototypes/Entities/Constructible/Ground/gascanisters.yml +++ b/Resources/Prototypes/Entities/Constructible/Ground/gascanisters.yml @@ -18,12 +18,101 @@ - type: GasCanister - type: Anchorable - type: Pullable + - type: UserInterface + - type: Appearance + + - type: entity parent: GasCanisterBase id: GasCanister name: Gas Canister + description: A canister that can contain any type of gas. It can be attached to connector ports using a wrench. components: - type: Sprite - sprite: "Constructible/Power/apc.rsi" - state: apc0 + netsync: false + sprite: Constructible/Atmos/canister.rsi + state: grey + - type: Appearance + visuals: + - type: GasCanisterVisualizer + stateConnected: can-connector + stateO0: can-o0 + stateO1: can-o1 + stateO2: can-o2 + stateO3: can-o3 + - type: UserInterface + interfaces: + - key: enum.GasCanisterUiKey.Key + type: GasCanisterBoundUserInterface + - type: Physics + mass: 25 + anchored: false + shapes: + - !type:PhysShapeAabb + bounds: "-0.5,-0.25,0.5,0.25" + mask: + - Impassable + - MobImpassable + - VaultImpassable + - SmallImpassable + layer: + - Opaque + - MobImpassable + - VaultImpassable + - SmallImpassable + +# Filled canisters, contain 1871.71051 moles each + +- type: entity + parent: GasCanister + id: AirCanister + name: Air Canister + components: + - type: Sprite + sprite: Constructible/Atmos/canister.rsi + state: grey + - type: GasCanister + gasMixture: + volume: 1000 + moles: + - 393.0592071 # oxygen 21% + - 1478.6513029 # nitrogen 79% + temperature: 293.15 + + + +- type: entity + parent: GasCanister + id: OxygenCanister + name: Oxygen Canister + components: + - type: Sprite + sprite: Constructible/Atmos/canister.rsi + state: blue + - type: GasCanister + gasMixture: + volume: 1000 + moles: + - 1871.71051 # oxygen + temperature: 293.15 + + + +- type: entity + parent: GasCanister + id: PhoronCanister + name: Phoron Canister + components: + - type: Sprite + sprite: Constructible/Atmos/canister.rsi + state: orange + - type: GasCanister + gasMixture: + volume: 1000 + moles: + - 0 # oxygen + - 0 # nitrogen + - 0 # carbon dioxide + - 1871.71051 # phoron + temperature: 293.15 diff --git a/Resources/Prototypes/Entities/Constructible/Ground/scrubbers.yml b/Resources/Prototypes/Entities/Constructible/Ground/scrubbers.yml index 2c6595a102..004aef4827 100644 --- a/Resources/Prototypes/Entities/Constructible/Ground/scrubbers.yml +++ b/Resources/Prototypes/Entities/Constructible/Ground/scrubbers.yml @@ -34,7 +34,7 @@ - type: NodeContainer nodes: - !type:PipeNode - nodeGroID: Pipe + nodeGroupID: Pipe pipeDirection: East - type: PressureSiphon scrubberOutletDirection: East diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/black-1.png b/Resources/Textures/Constructible/Atmos/canister.rsi/black-1.png new file mode 100644 index 0000000000000000000000000000000000000000..0cfd66e778cc836ea8f1baf5e7ca0b6ad80ea225 GIT binary patch literal 816 zcmV-01JC@4P)6IG3L(-1Vo;K5le+0blYYeMe84W6uH0Z!V}dn;9BYh9%Pb~`3*zBsMqT_jsw7aJ|{^MP1%l-B*{s?MQZ=GF+iu& zK}w11x&%Rh5F+zDkL7Ynr_*7(-Ll3zV9;{jd*{5&+V3FaeseL z9LF>o4c6;5hG8(9%_&Grd2{<7zP02@fcm#u`H`3 zB97zIS}A3oABtwP$!@m;pxtf*kftfm&(BOI6HN$_4F&@Mz7pRS(==%`8Z;V>lMlbWy|LTvSglrEU0so; zDMAPqi$&$x-EN2P`}BIf44~C&RRt)75JXW#6h*Ap>-_gH3~^mIKcR}P<2bmkOVROu zzd!2@lyaa}`{!0U9FNBUc_T{fkfv$=IZUM2*Vj@w6obJ4$8i`821HRrk|h6MK#?R#7!HS6mIeH3!K$QGd^ej7 zK@hN9F7w)6EEWJ~S3*gEYN@Kc_WM1iY33ph6RX{Bqw6}?*VlY}d{C>^kW$iWwJL8* z*LA`$WHOm(MWMNXBuTXSd|nb0$1%6Jw+x3v0G^(nSg+T_aZIgN%bV8W0nV)eIFf{i z%_WXwn$2dVBdWDRh)OFC!!U2f+42J=@i#lKp2KL@p8Ew zqBSk|?(Xhm1>|{d6F)yc@8z_=x3k~^rx~D>HoouU`1sf<=w`ElQVLH`Pm}RKxddd4 zK`G^w8~~J3V2n9&ows1UUJtf@A2&%7C+=hi7-P+D_4D~0v)Qcona}4v&TAL;o|Z10 zJCGy^mdhnP&l?=FFbrXgajt$+0AsA7l-m6-iUNcX1VMl>4DCLbWr;k`QI;jN)^i6h*zY zV;FTR##rNdo=r{&fz}$2kB{(uzsFTd;q>&>atI+v(-g)Sj1A$WfW=|~t+lOkyWNgf z)DEH$0)!A`SvD$QbZjZ5$g&LQ=jW)Z3IK3&asmMGJg>)Xhps3JyEcwvP)bpj<*?w< za$Xa~ag4=c0RYxy+Kvn%j^j5X-z{Kwh}x}~&1OiF#EPh@$_{6{#`8S*zCSsLPCn4Z zcDsFZDQz|zP)eOID`x>x%GOQ+VDL)joa5-|2%PiYb4n?uUd=`Y7-Nv8DORi1Xb>qS z>bmYFTdh|17>>coyj!maUglue^RKS1V2r`#+=(UErMek`KY9dcd&*uYno>){usf pWm$L4B%J>nwwQX)b4~ou;18B2cLe@hJnH}e002ovPDHLkV1my6c*p<% literal 0 HcmV?d00001 diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/blue-1.png b/Resources/Textures/Constructible/Atmos/canister.rsi/blue-1.png new file mode 100644 index 0000000000000000000000000000000000000000..bc5989e497f3bf3b006b86b7ead98fc61735d9f0 GIT binary patch literal 909 zcmV;819JR{P)HvgP+g17%*?o}oBwv-S6%CUZU`U!r(%Hd>nwi$ z5BYqaQt1&wN_X}3G>C^VejQ#`!PyO;;Py+Tb3t~}MO=Wi;k}Q|@`d&zrBaDfse}*$ zXX*#o`vpb{o&?xiZxi-$e1_a!B#}r&rfl1ebrQ%66w7n6TZeVxmu}Dkm+eJ#UGGA`$4CMGyvKtF`N-$i))I`( z5>M*1Ac(C}%2TWqx~`*?3PNx?458Nj=IU0_)jnx&E= z=XL6Zf~%AYLe_UPT3A@{dV#zMSHAo}%_vL)I@o00000NkvXXu0mjfxKXb7 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/blue.png b/Resources/Textures/Constructible/Atmos/canister.rsi/blue.png new file mode 100644 index 0000000000000000000000000000000000000000..28e7317cc498f84c31f65060fd204ccaa4946b45 GIT binary patch literal 952 zcmV;p14sOcP)z zL1^1n9L9g1OBo@th2$ZOZPGTZA=M#;9Y$zqFM};E9zC}7(rhO~Pr2+cG6p*h%3x6F zX>^ysU{JOi8cKU9q?DDE(ix|iP^Hu!#z`flB#jq_&<@XXEM`luT@QnOAcWuh-v9r- z|I_>4GyLC?kR?t|PWq~<0-$NyK#pZu0NA!oy9=na0ES`sj^jwhl@_3BS}$>OdVV0s17C1!n5Ox51z48V zOYH4g135vzRUItwjRkZ%9Zb{28@(IJ^SW=-H0g9YQUczQA#G2Ur2~QxyggNx;z}>U ztMd*Iz$sKc4#BuBK+b2xK6JkB-6*ceDxzZz$i99osa?Qsp9ObksH!Tp(+{XlKIjPv&3;$Tw&Mk) z3iK}nfa5sWw#`?1l22ZV3WTiJHN}`z;VlRl%ZvAki?H1g526BF3m%D|IjBI;f8t_z zYy{%Jfwd(OaHk2m8Q5v|oYB;E!3mr%RS-!7?%Gmp_{ih3R4VzEKR*_;djC_=xVIG@ zapXPmUcU*c0;P9vQ>j$mY!?)ZMYwf~%Gl@7ehfhJ=ttr`626oK2b8zbJuh-Il>YjG zVzEf2QW=^fb_vGE$KjVNFm+xe=$ecdRAd+)USa&)$++;*A7S~1hhZ384gz(TA~`)T zN*tj5Sp4p5yL|2>hG9^?;SmKn9{`pYCb)L#2<&bB<36DO{te`2(4YUv^1=km3lp+o zV+1trQND9vZ?#&DM_(4GFJ`!Q=?L}34E4nfilWf?{sR<62@Nmb$wK4azYwt7g0&^C zIu{~oj^nVoxfwE}tIh?n4PzWp?krfp$=ug~+ve=qO-4sYL*3PCmHqvFR##UkeD-V4 ztYi=^pt+JDKkJh|)@z#k?iuJQSbq-pUv^tx4)%xL7Evc*ynq0%3F%`{I3w0{yCIw| zK#;uuFmmN?#qqnKxsnL2W8q9V`7lV#9%L&Z0{Q$w&ya@V=}-M*Vj=zCu!p#No@C1sx5JusbkY<`e-SlpB0UtrFZ>}>;YupHTE|qQxWVNIaUI_D*vfzKHFoY072;l~1 z)<@qy?+7TR3?ps{aQ9)vEdj6m{{S=Vt+n?Am|0ILv92q%)~9zqW(?(=`@Zi0hzQqp zAtIDg0I0Qco+rm~{MwrmpsIb_Hr$ahUi5B0@?D q;7`L-<(#{!c2(`Ke!C%rXTAZj2uF@U#2?`R0000d2 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/can-o3.png b/Resources/Textures/Constructible/Atmos/canister.rsi/can-o3.png new file mode 100644 index 0000000000000000000000000000000000000000..fa7bf241fe2a6e096a83996ff5a97ae7b68a712f GIT binary patch literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJAWs*^kcv5P&nXHu81T4UjAuK| zyXv8t%(m^CyUY(TFFTXK$@F=?*AX@DNh*JJT$?M?*{1EgX13Aw9nVFkzm=9zTK!7v nd*!)85@bJBo}Z-RsVM&E2-mL59=4N!rZafD`njxgN@xNAVOcMG literal 0 HcmV?d00001 diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/can-oa1.png b/Resources/Textures/Constructible/Atmos/canister.rsi/can-oa1.png new file mode 100644 index 0000000000000000000000000000000000000000..8153aa5ae7d057f21bf09f3b504638ab038c44ca GIT binary patch literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJMo$;Vkcv6U2@b#qnR z17ukD*0M`V06~$B_{6329|>^q2BsubeCf#f8{*xPYAk!=l6#<1O2UCNT}zfV0TDg*F$|urelF{r5}E)iNItXx literal 0 HcmV?d00001 diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/can-open.png b/Resources/Textures/Constructible/Atmos/canister.rsi/can-open.png new file mode 100644 index 0000000000000000000000000000000000000000..369b2340aa9e75618bd6a199d62d69054635acca GIT binary patch literal 164 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJY)==*kcv5P&u!#ApuppD(R)f8 z-$ezr4@PhF71J0l2F|h*b$nN}-tMNnf)Wt?a&R#@wnAx9nECs=iRU7nIc5|uy2iD- z<88xs!?vUUKQn0aSnz$gH=D&~_u*?&RqqSVDK2dZI+Ysauzx$l!7mbrcV;VY1zOAC M>FVdQ&MBb@0AQ{@82|tP literal 0 HcmV?d00001 diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/grey-1.png b/Resources/Textures/Constructible/Atmos/canister.rsi/grey-1.png new file mode 100644 index 0000000000000000000000000000000000000000..167983cfe55677d4ea970dcd1b658e58d0fcd0c5 GIT binary patch literal 860 zcmV-i1Ec(jP)C5<6vuzQ+@acFgAO%A9dxRK&?$5(*;){?wx2-h2XHH|_X~7Z`T^=1EZ8k{=pb$m zg$~J(4nf4wmKvSvJ6xWJqgtMoK98a(JUl!|LH&LofL^aB1q+0aMx#M8 znT**+qfuf>fN7eGv$Hb*bY17<&?8aq2XU)S|hVj0jh4fs*JVzKzkm(S17Wo=be1t62j5Cj2_kB>|y6Z!0P zI;C2z(rUF9jYflPHXF}C6kXS`Z5!LR8IQ;EzHQqWh9OrdYOAU$nM?*iI-bwx8?66J z22!b%yfzGj-Q8VEr4j&x!C<8W*L6`<71wns6bkI^?QIlzrGO|RxY&SU7{&w&X0uuB z|96R%APhtK`?`@%PfueVASjp1sH#f2TqXzt-rnB+y?`h$FE6;Ri=rq1jK^cSF-3i6 zvl*`I^7{HJds}S0O*=0daaBvXo0QgbT=o}Ds znE}hP676<7CT2Jsa({o1>$(6O9vz|(2>2$hj7{=d1Nc4lmw(al2whG&}R|byq{{G%vD*#}B mf4{lT{$Ci4Mt{urZ}AVQXLfGm&V^e50000z zF>C8c6otR+ZeijgLc-W;)TRgoQmF_mq+5YOzaf=X`v+`P`xnS%>a7HVl&Zq3g1}1x zDWXCtBBuOHUVk9Z*OnC@qZDD2*|Qrt;%tnqK#u&))Bzj z*;$e2xlmkT0g9qj6HlkpR!;fe^E|>Z{C5SUXCcbsc++Yo+rfZ&46i|MGypyXNsaQ z91a_w&)yIO0j6oT?v^@)GvIk1)9IAKV8G?&Wh>|Y{+``#C%pPz0j6mdhG9@;4WCN z*VXrOg|aO3`0E$`JpED$sjt31=7$7mnnt}00C}F1Wf{MZf9AK3eFo~*8w3G{VW4T+ zj|)&$6#&b!h@yxnihz9vlKEd8{Xvz1^1WqQjj>(m?E%Meuq=x>j?s0UIF2i449DX= z>8tZ;nue;X71wc`UICrQrD>YQY&Ii`BCf8k0N89codJ)Jj{)#~A5GI(E|+xef=&UB z;}myyci6UFHQ4$2IRJyfpuyekb^z6Rp{gpjZDSZlbxgORw*-^P1lzWm&1L|V$&?v6 z1lzWmOeTHdy9Jb8T<(Cf6vN>V&+{q~FE1}ubCzQ!lL;3W7ri?{$Or1!Y&QQml&-I@ z(RH1ho11S9*Db(xU36Wqet$lcN=TB#Xf#5Sq{eey*Ku9<`vt7mYaGX6KA(36$@82f zNg8G}pU-g|r`HjMO5po`RpYjs@#EtoqtS?mhX-KaE57fGgzhcDk%*_KX-bkLBuUbE zzFx08J)l=WiL&;lY2vyrzVA1Bqco>_b`JH+Hw*Zj1=BPelOKY}>Ot^6@HJ2YREBg2 q%jL2@rWeBh4NLUh^F$~9&){z;41)dBPV>kB00007kFT&Wxn@3?Fc-aoS zxWO($RTzjMY)qXQtfDJzVhwK5Ld_1prb%qoy5VX4K*%ro{rcYb_g>!b6&f^X(4fKp zPVAz|;SM~2L@SJRigybcNau<9bG1n4FSZ-z#$}EHp4^628wTUBVmF~EvNzuCu#^*X zyDnthhzYM=Ayo^8JMa>%pv&TUECDmy@a!%;Txvp=WnSNNDuPyRv2HmZ2=?)9%0mB`5Lmh$R(KUsmqoc~aEXynoIII`oz6$FFNJ=pOBXH*PRBs00LHZVw zB%$j%nxD3HCwv|wSt*xyt$2A3<tdQFg+ifH+m>Yk^;be91>v$vO2UzUg4IgJ zbdN*Eb;@T)wo*kyMoXju05qwYInzG}@O+__2&D7G7BW#WANNbV?aQ*0ItHNcy~lb% zyzjgjlx_GoyKemzP!$2FYAR)l$a3`ux$BCOJ$n?-nWf56Jh3K@;`vq`?T*G^yXD!o zT}fHp&FIaZXhi)j4$phJo@Nvl0oCpJ)m9*4yqj~|@p4|=SL~*#TJ}#*PbdBHx&2yE zl*#@U=Y=mM{D)+ts7(2Dwb&>sH64d>9B1-E0RUZHU6U8h{|nh{_S$S;i9a1kLYa&Q R&Lsc<002ovPDHLkV1ntgk?a5f literal 0 HcmV?d00001 diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/orange.png b/Resources/Textures/Constructible/Atmos/canister.rsi/orange.png new file mode 100644 index 0000000000000000000000000000000000000000..c21c352f8fba199763c92e08d8aac2cf12c9252c GIT binary patch literal 838 zcmV-M1G)T(P)Bg?V^(=_2Yj#jX?Z9^%Ai;Ig?{>NI302pI~UA()d7O!>yob$oLn~TN-D?b2$W6mLjc-R13*BvbEdj43OaDMZy7alAC&+{OJ zKs`Tx;QH~g|kSRDmef^(CAJUJnalbA*F!f!$LgJoA7uqo;RH$|x+0 zj07oPAy12jw!5e40VH0R)f|HL0_|$erm@a#GU414dhR<^g1t6y?^9685y6{f;7HNf zOPekno{8UxZlBJ6-Jxfh0Xrwe(dbTIBeeu}hKSY+^g9%pH9J{sc23AQ4T%Aac5zq= zo;6St{-7*JYS^5ikDUP)pQfJ#EgguWbNl=I!25FxWd?k_wEmPXIe>!>)iT8Br4+(6 zO{|y(Ow){wml<)~I{>FoM`?R0nM5gtrt)KAw7o>WX^Nt>SqeuImy-5pf&?aB^}|8A`z;xo!bZ zh2YyY5=YwEri{QcMXZ=6QJMmd{Oa}cvr(E3(vqgx zHsHE0phZAO3-~G@6-wm$Dt|pxxpf?edc6*sV*R^OC`90)wpWH0YBfj__Jk-^5Cj2^ z;}8S^p64+=Jw2A;QUW@Zas2|8q1){i*1MvRf{ylFzevv;jY0(|_`Z+hIQYI#9LHQ< zUfzkojS)EL_4o}uwk(n)q0{N)7gOc8+wBqrLE*b7iarpYYu#4z~K??amA(a>jr za$wvFC=gIMD>*curdZcHqB7WGxE1fHng`ZpE8dl@xD`cN9LIS(9(x#lHViDwVonHN z!~F~+odgVzW|w(SiiE5osE wmm1a~glLZ>0MKYO+N0$Ef-ns4P4}Jf2a(MYP)8PqIsgCw07*qoM6N<$g5o$@l>h($ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/red.png b/Resources/Textures/Constructible/Atmos/canister.rsi/red.png new file mode 100644 index 0000000000000000000000000000000000000000..998a5d859e8c863fdb82e5e866dc8887344da885 GIT binary patch literal 765 zcmVz zv1{8v6vlt9iq{BAjG=SwU$PZw>LeKL!e|5sQ_DLpW zG{@(!1T2xJGT(@C93zC_L*Vdcnr0wBdPC*2^b`UX)pZ-VR3At)aQ#-&_(GNe{d~LR zTL)tg>?^_%&{0=C8N(n*jnw2@QbD!tzJ-9=R4=6Hy;f`n>1>dx~RF6T?`7mLl3xzx9yWGz^QuvNT zU&^d3XFN2g_Gtkt6&z()&4E0w`Tc$1V-U~*KI^9Z1JnvQCatB$ymuVeb$RJHxUO5c z-%%Wo7BD8=O1lPe_uHt#?;RMqX3PNw4*mjYkEnsH+1k($;Sr9T`+(~zK$EE;PYU!da%rZ vB1+(4AkD);(Z>ku|Arc-_dMIc{|tTs&NluL8O8^i00000NkvXXu0mjfL^NZ^ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/redws-1.png b/Resources/Textures/Constructible/Atmos/canister.rsi/redws-1.png new file mode 100644 index 0000000000000000000000000000000000000000..0dab9bdf7a5ead31bd13e4b955d92d4fdb31eaee GIT binary patch literal 905 zcmV;419tq0P)NVU96lBOzoVkdVs=Ej~;bl8X|o5TV+n;AjV1Lb9FIq4ZS$LFhxD-h2P|-uK@7 zV2B}x7-ERe9if{xHBEB>QmGV=*46-c4g3S4ufPGgr)fQMrBdley&ojtH*Bn}@eQyC zJO#cU89@*Pip3)3a+#&2CH7`#IrhoF>5v+PNY4+Nra7rp3Pn+<(9%ltJpMFRK*kbQb4U%g^sVF3U|QHVq$$g)f{8l_&ZW11!a z)oL|#r#k|ZlatW#`iW)nXBP=w*S&Ruc4{&{`G=JXs;bgxGyo_TivXyq%Erb9`FuVk zilXC2XTtJP3dmA6w-0I^t%a5&uU*R=>lAQp=O z?QlLGK=@lx^l~dn5&#n`E10Iq&dv_!=jYzPjYh*wi&H2Rh(@D;F9Lp~fj>G=z18x* zZ*Fdq&1O9h-P}r&#Kgn|fT#HK@^Szdx*{M50svpoF1hIFD06r3f^Ac=EbeEsJqmPP zN0KD(V2+QE4`jG60Y6;h{sY(#&1N(3zAXYNux%ULwmIt%@Z(uL9;g5pilQJ%5{jZ= znkE+)7q=pCZ3K4Tzvn;Tp&+zLot+`e?P_=P+iW&_NY(3g0K(z0SBe7@I6XZD;DmNb z)mts^l=n-;50=HTWsw9h3=%-{@Q`FO8K?k&v9Ym0A8?6!DJYdnq4o83pxsPPfWKQU zem4xS7fer2qw6|HM@LLot0cMzophWSv;uk}08p#d00P71ajS*U)e&8TX@afzIG3a5 zYsHI;i-A_0PNyByG`)5_@G$zVGX-ZXi=Pe;Kg}@mlYriF{L)9_yGrGQgsCMU0*w|MNwSMWHQfPBK`}>zoPhJ#z zL1^1n7{`B^2L=;}Ei((lLdAA)SLdcvXfsAmbFi+v^wRAz90Do3>`=Db4n7V-%q|NA zdg`rG;#;7kBPX9`vA9qn<3O-mD+**WE*R|~S+hG^dg*!?>;qx^^nd^FuTSs$zbE*= z$AT!ZP$+nkBmt1kW@lph{XPJW+?z>2|xZ{2vN2 z0+J+!PgSecyjhq^rLq7}EEYZ2b%l6^1!S|?FmbtDo(J;)4h{~mEbILW==b|!;_K_{ znV9kY{r!DWS}_5G!2rv$Xf~Tdq?Tnd7z~84ABcde)v7Qd_=2m|st{KA6_}9W>3;b$zZO39kWwm!%Rzz$p}P9EY(rJB|ZDM^!ny2_b@AFloT!1d=3S zetyF5g_BUo*_=85O29I37nt-l*L86mhacqxU;gP=VA6W6V9xUU5U|>40B|4Z1Q+-f zc=g?gg(rj+7~jAD*4(IwTmu)n&fcwCeBEepqN=RomwD`rmMr_g!1UC_}W_^7ffIkX_NW#xrEdY*=j!+bZpGqa3N92hV@I83H0Y5hy z^m;u2R#sL3NG6k0*x_&p5bhVbT#jv3tO7GEb~jZ z%eS)34>#nCY{S#7EmkM~&pYF!(`jxe5~S1Vsqxi-%s;rp)Le z@HpUjX8{26))r6s+kj?yee>di+ld6vUi&Yl13V4p#zmw6UDp@d?KVoS7D@8r%^OZf zBTh%7a7?MyXt&!$?Svq>2u;(ZSgY}cqVQ|b4U=o*Rq%TTd|I!Qux;Kx#LNq!4--w( zyp4?w6h#4ilL1&Ixt9KE(Iv-I*${lex`|G5Mh85n+)3kgo_n35Y1dYNOR^4#-No0X5$ z%SgS*eWVj^@ZeqIR|7?*rRR1VFIctmoTL!D?xI z;%DB3X+}M&Jf$ecqx0D)<n8{JSxc zIW6J;RF4}Cm+YBZjy;?A>!-pdF`&wZ<7Mh9O65WGW~TlRXALPe+4K8bN!OX2Ee*S; l&%DODY`=iu+PViT<&XFB9+~UseFUh5!PC{xWt~$(695C}GlKvC literal 0 HcmV?d00001 diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/yellow-1.png b/Resources/Textures/Constructible/Atmos/canister.rsi/yellow-1.png new file mode 100644 index 0000000000000000000000000000000000000000..707993bbcc376af3591ba3031a4d037169ab2112 GIT binary patch literal 861 zcmV-j1ETziP)Xr8#q0Azq%~DsnGXzeC--vB_nvdlxq&WS zx^(ICyOY{%vQQud*wqikqWtag6DXHSskwHP%bVLxvh9100ZyNW%6k|cfwSk*=(-k; zkNHeb%Q@GD1-HpW*r||~1%(3Pu723FSALIvW;R}HSuz6Z zd?<#7Gl*IM!1ZfB;4?J5NU0QX^9Cth*F)R30Z1ozKRzbM)6>#uu?QbNeu;Mf;o}z; zTf-lrr9b$F?a-Cyj0@=6{u1vV01+7Fo<$1gpiJfz%)&S5O`j&^$7ItjK`xR zkn7EG_|Ohuh1akAwgw!>!7vOQ$0483)6>(_k>j=kk|^iu0Ooq*gO#LSuQ$E7N;>&b zTa2Ie2kCfmXHx|zY}>{#3~bv*2*L95@{bEhQYo+U|EI5LnkL27Y77laQ7XUndL74c zSXx?&YTNTXpz}&-NL3kmMhdxjFr^nrp}0oZi-P65sbHHWdKS n*Vi|(+5FE?tyX`T?Je;Q{-a5<{Yh$i00000NkvXXu0mjft;CsT literal 0 HcmV?d00001 diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/yellow.png b/Resources/Textures/Constructible/Atmos/canister.rsi/yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..d4ca097a7f6e7dbbabe10f41f63129200727abe1 GIT binary patch literal 844 zcmV-S1GD^zP)X3yUB{*5V&nV%j~FLJxvRg*|OgdQ#fb-g;>-E&3bM9u}0I1aFH_ zX;9jWg0(i3&4L70S!WsrStx|`@SNSvb~l+?4~2dZlAXNw&6_vxeUkT+HZ%te(DSfQM2mrnw=GM|sjAkj7orM?#0XXM)VJLWb zK^f?j-ddG>MzhxtK=zWPSOkuIx&X>R<8g?A_h@B6ye}5xzDY=}fpVD$eIGFMbQj9# z83q|$D&J_9CKa~ja(V!X$E9T=Ux71(yz+LqnDc5w=y%m^@cJG~R6bf6QB)8D@bUcNG5=7D!_jX$MJ4q$7G_EMnx zP!dp86}dYKR8@_Qn|b26asc&XYNe&cWRf5V;2wrCFH*0NToOgwXbVz5tggj;QX&1{P$2C)Pj=#e2EPFH WYC<~Z7