#nullable enable using System; using Content.Server.Atmos; using Content.Server.GameObjects.Components.Atmos.Piping; using Content.Server.Interfaces; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Components; using Robust.Shared.GameObjects.Components.Transform; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; 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 Content.Shared.GameObjects.EntitySystems.ActionBlocker; 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] [ComponentReference(typeof(IActivate))] public class GasCanisterComponent : Component, IGasMixtureHolder, IActivate { public override string Name => "GasCanister"; 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; } [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(); if (Owner.TryGetComponent(out var physics)) { AnchorUpdate(); } if (UserInterface != null) { UserInterface.OnReceiveMessage += OnUiReceiveMessage; } // Init some variables Label = Owner.Name; Owner.TryGetComponent(out _appearance); UpdateUserInterface(); UpdateAppearance(); } public override void HandleMessage(ComponentMessage message, IComponent? component) { base.HandleMessage(message, component); switch (message) { case AnchoredChangedMessage: AnchorUpdate(); break; } } #region Connector port methods public override void OnRemove() { base.OnRemove(); 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) .FirstOrDefault(); if (port == null) return; ConnectedPort = port; ConnectedPort.ConnectGasCanister(this); } public void DisconnectFromPort() { ConnectedPort?.DisconnectGasCanister(); ConnectedPort = null; } private void AnchorUpdate() { if (Anchored) { TryConnectToPort(); } else { 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(); } } } }