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 0000000000..0cfd66e778
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/black-1.png differ
diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/black.png b/Resources/Textures/Constructible/Atmos/canister.rsi/black.png
new file mode 100644
index 0000000000..88a571ea84
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/black.png differ
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 0000000000..bc5989e497
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/blue-1.png differ
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 0000000000..28e7317cc4
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/blue.png differ
diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/can-connector.png b/Resources/Textures/Constructible/Atmos/canister.rsi/can-connector.png
new file mode 100644
index 0000000000..0ce73b87f3
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/can-connector.png differ
diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/can-o0.png b/Resources/Textures/Constructible/Atmos/canister.rsi/can-o0.png
new file mode 100644
index 0000000000..3e6c3657fc
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/can-o0.png differ
diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/can-o1.png b/Resources/Textures/Constructible/Atmos/canister.rsi/can-o1.png
new file mode 100644
index 0000000000..bdd7f13328
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/can-o1.png differ
diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/can-o2.png b/Resources/Textures/Constructible/Atmos/canister.rsi/can-o2.png
new file mode 100644
index 0000000000..f8c0937b1f
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/can-o2.png differ
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 0000000000..fa7bf241fe
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/can-o3.png differ
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 0000000000..8153aa5ae7
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/can-oa1.png differ
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 0000000000..369b2340aa
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/can-open.png differ
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 0000000000..167983cfe5
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/grey-1.png differ
diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/grey.png b/Resources/Textures/Constructible/Atmos/canister.rsi/grey.png
new file mode 100644
index 0000000000..6a046f8ce1
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/grey.png differ
diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/meta.json b/Resources/Textures/Constructible/Atmos/canister.rsi/meta.json
new file mode 100644
index 0000000000..4a22cf8e66
--- /dev/null
+++ b/Resources/Textures/Constructible/Atmos/canister.rsi/meta.json
@@ -0,0 +1,201 @@
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from https://github.com/discordia-space/CEV-Eris",
+ "states": [
+ {
+ "name": "black",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "black-1",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "blue",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "blue-1",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "can-connector",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "can-o0",
+ "directions": 1,
+ "delays": [
+ [
+ 0.2,
+ 0.2
+ ]
+ ]
+ },
+ {
+ "name": "can-o1",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "can-o2",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "can-o3",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "can-oa1",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "can-open",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "grey",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "grey-1",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "orange",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "orange-1",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "red",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "red-1",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "redws",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "redws-1",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "yellow",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "yellow-1",
+ "directions": 1,
+ "delays": [
+ [
+ 1
+ ]
+ ]
+ }
+ ]
+}
diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/orange-1.png b/Resources/Textures/Constructible/Atmos/canister.rsi/orange-1.png
new file mode 100644
index 0000000000..420eba60af
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/orange-1.png differ
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 0000000000..c21c352f8f
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/orange.png differ
diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/red-1.png b/Resources/Textures/Constructible/Atmos/canister.rsi/red-1.png
new file mode 100644
index 0000000000..fada97a48f
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/red-1.png differ
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 0000000000..998a5d859e
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/red.png differ
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 0000000000..0dab9bdf7a
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/redws-1.png differ
diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/redws.png b/Resources/Textures/Constructible/Atmos/canister.rsi/redws.png
new file mode 100644
index 0000000000..4b28f64788
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/redws.png differ
diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/scrubber-connector.png b/Resources/Textures/Constructible/Atmos/canister.rsi/scrubber-connector.png
new file mode 100644
index 0000000000..7e3822babe
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/scrubber-connector.png differ
diff --git a/Resources/Textures/Constructible/Atmos/canister.rsi/scrubber-open.png b/Resources/Textures/Constructible/Atmos/canister.rsi/scrubber-open.png
new file mode 100644
index 0000000000..4eb9d1e708
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/scrubber-open.png differ
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 0000000000..707993bbcc
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/yellow-1.png differ
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 0000000000..d4ca097a7f
Binary files /dev/null and b/Resources/Textures/Constructible/Atmos/canister.rsi/yellow.png differ