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