]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predicted internals (#33800)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Fri, 2 May 2025 08:22:29 +0000 (18:22 +1000)
committerGitHub <noreply@github.com>
Fri, 2 May 2025 08:22:29 +0000 (18:22 +1000)
* Predicted gas pumps

I wanted to try out atmos and first thing I found.

* a

* Atmos device prediction

- Canisters
- Tanks
- Internals

AirMixes aren't predicted so nothing on that front but all the UIs should be a lot closer.

* Remove details range

* Gas tank prediction

* Even more sweeping changes

* Alerts

* rehg

* Popup fix

* Fix merge conflicts

* Fix

* Review

94 files changed:
Content.Client/Alerts/ClientAlertsSystem.cs
Content.Client/Atmos/EntitySystems/GasTankSystem.cs [new file with mode: 0644]
Content.Client/Atmos/Piping/Unary/Systems/GasCanisterSystem.cs [new file with mode: 0644]
Content.Client/Atmos/UI/GasCanisterBoundUserInterface.cs
Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs
Content.Client/Body/Systems/InternalsSystem.cs [new file with mode: 0644]
Content.Client/UserInterface/Systems/Alerts/Controls/AlertControl.cs
Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankBoundUserInterface.cs
Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankWindow.cs
Content.IntegrationTests/Tests/Power/PowerTest.cs
Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
Content.Server/Ame/AmeNodeGroup.cs
Content.Server/Ame/EntitySystems/AmeControllerSystem.cs
Content.Server/Atmos/Components/BreathToolComponent.cs [deleted file]
Content.Server/Atmos/Components/GasTankComponent.cs [deleted file]
Content.Server/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs
Content.Server/Atmos/EntitySystems/AtmosphereSystem.BreathTool.cs [deleted file]
Content.Server/Atmos/EntitySystems/AtmosphereSystem.GridAtmosphere.cs
Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs
Content.Server/Atmos/EntitySystems/GasTankSystem.cs
Content.Server/Atmos/EntitySystems/PipeRestrictOverlapSystem.cs
Content.Server/Atmos/IGasMixtureHolder.cs [deleted file]
Content.Server/Atmos/Piping/EntitySystems/AtmosPipeAppearanceSystem.cs
Content.Server/Atmos/Piping/EntitySystems/AtmosUnsafeUnanchorSystem.cs
Content.Server/Atmos/Piping/Unary/Components/GasCanisterComponent.cs [deleted file]
Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs
Content.Server/Body/Components/InternalsComponent.cs [deleted file]
Content.Server/Body/Systems/InternalsSystem.cs
Content.Server/Body/Systems/LungSystem.cs
Content.Server/DeviceLinking/Systems/PowerSensorSystem.cs
Content.Server/DeviceNetwork/Components/ApcNetworkComponent.cs
Content.Server/DeviceNetwork/Systems/ApcNetworkSystem.cs
Content.Server/Electrocution/ElectrocutionNode.cs
Content.Server/Electrocution/ElectrocutionSystem.cs
Content.Server/Movement/Systems/JetpackSystem.cs
Content.Server/NodeContainer/EntitySystems/NodeContainerSystem.cs
Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs
Content.Server/NodeContainer/NodeContainerComponent.cs [deleted file]
Content.Server/NodeContainer/NodeGroups/BaseNodeGroup.cs
Content.Server/NodeContainer/NodeGroups/NodeGroupAttribute.cs
Content.Server/NodeContainer/NodeGroups/NodeGroupFactory.cs
Content.Server/NodeContainer/NodeGroups/PipeNet.cs
Content.Server/NodeContainer/Nodes/AdjacentNode.cs
Content.Server/NodeContainer/Nodes/IRotatableNode.cs
Content.Server/NodeContainer/Nodes/Node.cs [deleted file]
Content.Server/NodeContainer/Nodes/NodeHelpers.cs
Content.Server/NodeContainer/Nodes/PipeNode.cs
Content.Server/NodeContainer/Nodes/PortPipeNode.cs
Content.Server/NodeContainer/Nodes/PortablePipeNode.cs
Content.Server/PneumaticCannon/PneumaticCannonSystem.cs
Content.Server/Power/Components/BaseNetConnectorComponent.cs
Content.Server/Power/Components/PowerMonitoringDeviceComponent.cs
Content.Server/Power/EntitySystems/CableMultitoolSystem.cs
Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs
Content.Server/Power/Generation/Teg/TegNodeGroup.cs
Content.Server/Power/Generation/Teg/TegSystem.cs
Content.Server/Power/Generator/PowerSwitchableSystem.cs
Content.Server/Power/NodeGroups/ApcNet.cs
Content.Server/Power/NodeGroups/BaseNetConnectorNodeGroup.cs
Content.Server/Power/NodeGroups/BasePowerNet.cs
Content.Server/Power/NodeGroups/PowerNet.cs
Content.Server/Power/Nodes/CableDeviceNode.cs
Content.Server/Power/Nodes/CableNode.cs
Content.Server/Power/Nodes/CableTerminalNode.cs
Content.Server/Power/Nodes/CableTerminalPortNode.cs
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenAutoCabling.cs
Content.Server/Sandbox/Commands/ColorNetworkCommand.cs
Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs
Content.Shared/Alert/AlertsSystem.cs
Content.Shared/Atmos/Components/BreathToolComponent.cs [new file with mode: 0644]
Content.Shared/Atmos/Components/GasTankComponent.cs [new file with mode: 0644]
Content.Shared/Atmos/Components/SharedGasTankComponent.cs
Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.BreathTool.cs [new file with mode: 0644]
Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs
Content.Shared/Atmos/EntitySystems/SharedGasTankSystem.cs [new file with mode: 0644]
Content.Shared/Atmos/IGasMixtureHolder.cs [new file with mode: 0644]
Content.Shared/Atmos/Piping/Binary/Components/SharedGasCanisterComponent.cs
Content.Shared/Atmos/Piping/Unary/Components/GasCanisterComponent.cs [new file with mode: 0644]
Content.Shared/Atmos/Piping/Unary/Systems/SharedGasCanisterSystem.cs [new file with mode: 0644]
Content.Shared/Body/Components/InternalsComponent.cs [new file with mode: 0644]
Content.Shared/Body/Systems/SharedInternalsSystem.cs [new file with mode: 0644]
Content.Shared/Clothing/EntitySystems/MaskSystem.cs
Content.Shared/Lock/ActivatableUIRequiresLockComponent.cs
Content.Shared/Lock/LockSystem.cs
Content.Shared/NodeContainer/Node.cs [new file with mode: 0644]
Content.Shared/NodeContainer/NodeContainerComponent.cs [new file with mode: 0644]
Content.Shared/NodeContainer/NodeGroups/INodeGroup.cs [new file with mode: 0644]
Content.Shared/NodeContainer/NodeGroups/NodeGroupID.cs [new file with mode: 0644]
Content.Shared/Timing/UseDelaySystem.cs
Content.Shared/UserInterface/ActivatableUIRequiresAnchorSystem.cs
Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml
Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml
Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml

index 5b658fb07c2ae903caa3192052dca13ed4e9a4fe..43c74adc5dbb8da05c0834fca4a14bd3b082ba2d 100644 (file)
@@ -2,6 +2,7 @@ using System.Linq;
 using Content.Shared.Alert;
 using JetBrains.Annotations;
 using Robust.Client.Player;
+using Robust.Client.UserInterface;
 using Robust.Shared.GameStates;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
@@ -15,6 +16,7 @@ public sealed class ClientAlertsSystem : AlertsSystem
 
     [Dependency] private readonly IPlayerManager _playerManager = default!;
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly IUserInterfaceManager _ui = default!;
 
     public event EventHandler? ClearAlerts;
     public event EventHandler<IReadOnlyDictionary<AlertKey, AlertState>>? SyncAlerts;
@@ -27,6 +29,12 @@ public sealed class ClientAlertsSystem : AlertsSystem
         SubscribeLocalEvent<AlertsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
         SubscribeLocalEvent<AlertsComponent, ComponentHandleState>(OnHandleState);
     }
+
+    protected override void HandledAlert()
+    {
+        _ui.ClickSound();
+    }
+
     protected override void LoadPrototypes()
     {
         base.LoadPrototypes();
diff --git a/Content.Client/Atmos/EntitySystems/GasTankSystem.cs b/Content.Client/Atmos/EntitySystems/GasTankSystem.cs
new file mode 100644 (file)
index 0000000..696e793
--- /dev/null
@@ -0,0 +1,29 @@
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.EntitySystems;
+
+namespace Content.Client.Atmos.EntitySystems;
+
+public sealed class GasTankSystem : SharedGasTankSystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<GasTankComponent, AfterAutoHandleStateEvent>(OnGasTankState);
+    }
+
+    private void OnGasTankState(Entity<GasTankComponent> ent, ref AfterAutoHandleStateEvent args)
+    {
+        if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui))
+        {
+            bui.Update<GasTankBoundUserInterfaceState>();
+        }
+    }
+
+    public override void UpdateUserInterface(Entity<GasTankComponent> ent)
+    {
+        if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui))
+        {
+            bui.Update<GasTankBoundUserInterfaceState>();
+        }
+    }
+}
diff --git a/Content.Client/Atmos/Piping/Unary/Systems/GasCanisterSystem.cs b/Content.Client/Atmos/Piping/Unary/Systems/GasCanisterSystem.cs
new file mode 100644 (file)
index 0000000..cae184e
--- /dev/null
@@ -0,0 +1,32 @@
+using Content.Client.Atmos.UI;
+using Content.Shared.Atmos.Piping.Binary.Components;
+using Content.Shared.Atmos.Piping.Unary.Components;
+using Content.Shared.Atmos.Piping.Unary.Systems;
+using Content.Shared.NodeContainer;
+
+namespace Content.Client.Atmos.Piping.Unary.Systems;
+
+public sealed class GasCanisterSystem : SharedGasCanisterSystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<GasCanisterComponent, AfterAutoHandleStateEvent>(OnGasState);
+    }
+
+    private void OnGasState(Entity<GasCanisterComponent> ent, ref AfterAutoHandleStateEvent args)
+    {
+        if (UI.TryGetOpenUi<GasCanisterBoundUserInterface>(ent.Owner, GasCanisterUiKey.Key, out var bui))
+        {
+            bui.Update<GasCanisterBoundUserInterfaceState>();
+        }
+    }
+
+    protected override void DirtyUI(EntityUid uid, GasCanisterComponent? component = null, NodeContainerComponent? nodes = null)
+    {
+        if (UI.TryGetOpenUi<GasCanisterBoundUserInterface>(uid, GasCanisterUiKey.Key, out var bui))
+        {
+            bui.Update<GasCanisterBoundUserInterfaceState>();
+        }
+    }
+}
index 7bf9b396d5e02d3bc9eed20767380a7d4341a84d..0456426b1fc5780a10b0bedb11fa699364b7a273 100644 (file)
@@ -1,4 +1,7 @@
-using Content.Shared.Atmos.Piping.Binary.Components;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Piping.Binary.Components;
+using Content.Shared.Atmos.Piping.Unary.Components;
+using Content.Shared.IdentityManagement;
 using JetBrains.Annotations;
 using Robust.Client.GameObjects;
 using Robust.Client.UserInterface;
@@ -32,22 +35,22 @@ namespace Content.Client.Atmos.UI
 
         private void OnTankEjectPressed()
         {
-            SendMessage(new GasCanisterHoldingTankEjectMessage());
+            SendPredictedMessage(new GasCanisterHoldingTankEjectMessage());
         }
 
         private void OnReleasePressureSet(float value)
         {
-            SendMessage(new GasCanisterChangeReleasePressureMessage(value));
+            SendPredictedMessage(new GasCanisterChangeReleasePressureMessage(value));
         }
 
         private void OnReleaseValveOpenPressed()
         {
-            SendMessage(new GasCanisterChangeReleaseValveMessage(true));
+            SendPredictedMessage(new GasCanisterChangeReleaseValveMessage(true));
         }
 
         private void OnReleaseValveClosePressed()
         {
-            SendMessage(new GasCanisterChangeReleaseValveMessage(false));
+            SendPredictedMessage(new GasCanisterChangeReleaseValveMessage(false));
         }
 
         /// <summary>
@@ -57,17 +60,21 @@ namespace Content.Client.Atmos.UI
         protected override void UpdateState(BoundUserInterfaceState state)
         {
             base.UpdateState(state);
-            if (_window == null || state is not GasCanisterBoundUserInterfaceState cast)
+            if (_window == null || state is not GasCanisterBoundUserInterfaceState cast || !EntMan.TryGetComponent(Owner, out GasCanisterComponent? component))
                 return;
 
-            _window.SetCanisterLabel(cast.CanisterLabel);
+            var canisterLabel = Identity.Name(Owner, EntMan);
+            var tankLabel = component.GasTankSlot.Item != null ? Identity.Name(component.GasTankSlot.Item.Value, EntMan) : null;
+
+            _window.SetCanisterLabel(canisterLabel);
             _window.SetCanisterPressure(cast.CanisterPressure);
             _window.SetPortStatus(cast.PortStatus);
-            _window.SetTankLabel(cast.TankLabel);
+
+            _window.SetTankLabel(tankLabel);
             _window.SetTankPressure(cast.TankPressure);
-            _window.SetReleasePressureRange(cast.ReleasePressureMin, cast.ReleasePressureMax);
-            _window.SetReleasePressure(cast.ReleasePressure);
-            _window.SetReleaseValve(cast.ReleaseValve);
+            _window.SetReleasePressureRange(component.MinReleasePressure, component.MaxReleasePressure);
+            _window.SetReleasePressure(component.ReleasePressure);
+            _window.SetReleaseValve(component.ReleaseValve);
         }
 
         protected override void Dispose(bool disposing)
index 3c3d8f15091f8a9fcabfe2f4cc4cbb7ad7c5120e..b959bc966b029a73acdd15c8708b67bf82fd28fd 100644 (file)
@@ -13,9 +13,6 @@ namespace Content.Client.Atmos.UI;
 [UsedImplicitly]
 public sealed class GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
 {
-    [ViewVariables]
-    private const float MaxPressure = Atmospherics.MaxOutputPressure;
-
     [ViewVariables]
     private GasPressurePumpWindow? _window;
 
diff --git a/Content.Client/Body/Systems/InternalsSystem.cs b/Content.Client/Body/Systems/InternalsSystem.cs
new file mode 100644 (file)
index 0000000..87daac3
--- /dev/null
@@ -0,0 +1,24 @@
+using Content.Shared.Atmos.Components;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Systems;
+
+namespace Content.Client.Body.Systems;
+
+public sealed class InternalsSystem : SharedInternalsSystem
+{
+    [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<InternalsComponent, AfterAutoHandleStateEvent>(OnInternalsAfterState);
+    }
+
+    private void OnInternalsAfterState(Entity<InternalsComponent> ent, ref AfterAutoHandleStateEvent args)
+    {
+        if (ent.Comp.GasTankEntity != null && _ui.TryGetOpenUi(ent.Comp.GasTankEntity.Value, SharedGasTankUiKey.Key, out var bui))
+        {
+            bui.Update();
+        }
+    }
+}
index b0e2e394833da54e7c6a9b03e465081074184f10..4e803a20b1573681084efd85c5153b749731f1b2 100644 (file)
@@ -12,6 +12,8 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
 {
     public sealed class AlertControl : BaseButton
     {
+        [Dependency] private readonly IEntityManager _entityManager = default!;
+
         public AlertPrototype Alert { get; }
 
         /// <summary>
@@ -33,8 +35,7 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
         private (TimeSpan Start, TimeSpan End)? _cooldown;
 
         private short? _severity;
-        private readonly IGameTiming _gameTiming;
-        private readonly IEntityManager _entityManager;
+
         private readonly SpriteView _icon;
         private readonly CooldownGraphic _cooldownGraphic;
 
@@ -47,8 +48,10 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
         /// <param name="severity">severity of alert, null if alert doesn't have severity levels</param>
         public AlertControl(AlertPrototype alert, short? severity)
         {
-            _gameTiming = IoCManager.Resolve<IGameTiming>();
-            _entityManager = IoCManager.Resolve<IEntityManager>();
+            // Alerts will handle this.
+            MuteSounds = true;
+
+            IoCManager.InjectDependencies(this);
             TooltipSupplier = SupplyTooltip;
             Alert = alert;
             _severity = severity;
index 4ae74a5d65e5aba60df097e90d9add26b06fa162..19e578fda3886bea51b022bb2d6c124d03e69f26 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.EntitySystems;
 using JetBrains.Annotations;
 using Robust.Client.GameObjects;
 using Robust.Client.UserInterface;
@@ -17,7 +18,7 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank
 
         public void SetOutputPressure(float value)
         {
-            SendMessage(new GasTankSetPressureMessage
+            SendPredictedMessage(new GasTankSetPressureMessage
             {
                 Pressure = value
             });
@@ -25,13 +26,14 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank
 
         public void ToggleInternals()
         {
-            SendMessage(new GasTankToggleInternalsMessage());
+            SendPredictedMessage(new GasTankToggleInternalsMessage());
         }
 
         protected override void Open()
         {
             base.Open();
             _window = this.CreateWindow<GasTankWindow>();
+            _window.Entity = Owner;
             _window.SetTitle(EntMan.GetComponent<MetaDataComponent>(Owner).EntityName);
             _window.OnOutputPressure += SetOutputPressure;
             _window.OnToggleInternals += ToggleInternals;
@@ -41,6 +43,12 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank
         {
             base.UpdateState(state);
 
+            if (EntMan.TryGetComponent(Owner, out GasTankComponent? component))
+            {
+                var canConnect = EntMan.System<SharedGasTankSystem>().CanConnectToInternals((Owner, component));
+                _window?.Update(canConnect, component.IsConnected, component.OutputPressure);
+            }
+
             if (state is GasTankBoundUserInterfaceState cast)
                 _window?.UpdateState(cast);
         }
index fd5624ad8a7c621b952fa504000491e20ffc0a10..638df36504ce6b4a7778c84b72cbcf570e7b4f6d 100644 (file)
@@ -3,11 +3,14 @@ using Content.Client.Message;
 using Content.Client.Resources;
 using Content.Client.Stylesheets;
 using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.EntitySystems;
+using Content.Shared.Timing;
 using Robust.Client.Graphics;
 using Robust.Client.ResourceManagement;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Timing;
 using static Robust.Client.UserInterface.Controls.BoxContainer;
 
 namespace Content.Client.UserInterface.Systems.Atmos.GasTank;
@@ -15,6 +18,7 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank;
 public sealed class GasTankWindow
     : BaseWindow
 {
+    [Dependency] private readonly IEntityManager _entManager = default!;
     [Dependency] private readonly IResourceCache _cache = default!;
 
     private readonly RichTextLabel _lblPressure;
@@ -23,6 +27,8 @@ public sealed class GasTankWindow
     private readonly Button _btnInternals;
     private readonly Label _topLabel;
 
+    public EntityUid Entity;
+
     public event Action<float>? OnOutputPressure;
     public event Action? OnToggleInternals;
 
@@ -194,12 +200,30 @@ public sealed class GasTankWindow
     public void UpdateState(GasTankBoundUserInterfaceState state)
     {
         _lblPressure.SetMarkup(Loc.GetString("gas-tank-window-tank-pressure-text", ("tankPressure", $"{state.TankPressure:0.##}")));
-        _btnInternals.Disabled = !state.CanConnectInternals;
+    }
+
+    public void Update(bool canConnectInternals, bool internalsConnected, float outputPressure)
+    {
+        _btnInternals.Disabled = !canConnectInternals;
         _lblInternals.SetMarkup(Loc.GetString("gas-tank-window-internal-text",
-            ("status", Loc.GetString(state.InternalsConnected ? "gas-tank-window-internal-connected" : "gas-tank-window-internal-disconnected"))));
-        if (state.OutputPressure.HasValue)
+            ("status", Loc.GetString(internalsConnected ? "gas-tank-window-internal-connected" : "gas-tank-window-internal-disconnected"))));
+        _spbPressure.Value = outputPressure;
+    }
+
+    protected override void FrameUpdate(FrameEventArgs args)
+    {
+        base.FrameUpdate(args);
+
+        // Easier than managing state on any ent changes. Previously this was just ticked on server's GasTankSystem.
+        if (_entManager.TryGetComponent(Entity, out GasTankComponent? tank))
+        {
+            var canConnectInternals = _entManager.System<SharedGasTankSystem>().CanConnectToInternals((Entity, tank));
+            _btnInternals.Disabled = !canConnectInternals;
+        }
+
+        if (!_btnInternals.Disabled)
         {
-            _spbPressure.Value = state.OutputPressure.Value;
+            _btnInternals.Disabled = _entManager.System<UseDelaySystem>().IsDelayed(Entity, id: SharedGasTankSystem.GasTankDelay);
         }
     }
 
index 55bb42f8ced4c77ea1ced0113825cc8830bd32bf..a448427d050323fbbd3488c1387a3ccbc0d04827 100644 (file)
@@ -6,6 +6,7 @@ using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Server.Power.Nodes;
 using Content.Shared.Coordinates;
+using Content.Shared.NodeContainer;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Map;
 using Robust.Shared.Maths;
index 8b0a24d9029f670e30d000c08bbdee1768cff649..ccc7a98a1e81788a9f08bf291085bdb945bd438a 100644 (file)
@@ -18,6 +18,7 @@ using Content.Shared.Access.Components;
 using Content.Shared.Access.Systems;
 using Content.Shared.Administration;
 using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
 using Content.Shared.Construction.Components;
 using Content.Shared.Damage;
 using Content.Shared.Damage.Components;
index f4eb1bb302f4819cca1135e7c3153b66158cecab..f9cec6c5e7d9a851b8de55182f8afb1f109ab837 100644 (file)
@@ -5,6 +5,8 @@ using Content.Server.Chat.Managers;
 using Content.Server.Explosion.EntitySystems;
 using Content.Server.NodeContainer.NodeGroups;
 using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
 using Robust.Server.GameObjects;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Random;
index 7e84cbc7430c54115a7680eb3a2fba25a740e9e2..ff6cdd2da77a76bc0ed39f11731a822325b3134b 100644 (file)
@@ -10,6 +10,7 @@ using Content.Shared.Ame.Components;
 using Content.Shared.Containers.ItemSlots;
 using Content.Shared.Database;
 using Content.Shared.Mind.Components;
+using Content.Shared.NodeContainer;
 using Content.Shared.Power;
 using Robust.Server.GameObjects;
 using Robust.Shared.Audio;
diff --git a/Content.Server/Atmos/Components/BreathToolComponent.cs b/Content.Server/Atmos/Components/BreathToolComponent.cs
deleted file mode 100644 (file)
index ae17a5d..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-using Content.Shared.Inventory;
-
-namespace Content.Server.Atmos.Components
-{
-    /// <summary>
-    /// Used in internals as breath tool.
-    /// </summary>
-    [RegisterComponent]
-    [ComponentProtoName("BreathMask")]
-    public sealed partial class BreathToolComponent : Component
-    {
-        /// <summary>
-        /// Tool is functional only in allowed slots
-        /// </summary>
-        [DataField]
-        public SlotFlags AllowedSlots = SlotFlags.MASK | SlotFlags.HEAD;
-        public bool IsFunctional;
-
-        public EntityUid? ConnectedInternalsEntity;
-    }
-}
diff --git a/Content.Server/Atmos/Components/GasTankComponent.cs b/Content.Server/Atmos/Components/GasTankComponent.cs
deleted file mode 100644 (file)
index 2d6b073..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-using Content.Shared.Atmos;
-using Robust.Shared.Audio;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Atmos.Components
-{
-    [RegisterComponent]
-    public sealed partial class GasTankComponent : Component, IGasMixtureHolder
-    {
-        public const float MaxExplosionRange = 26f;
-        private const float DefaultLowPressure = 0f;
-        private const float DefaultOutputPressure = Atmospherics.OneAtmosphere;
-
-        public int Integrity = 3;
-        public bool IsLowPressure => (Air?.Pressure ?? 0F) <= TankLowPressure;
-
-        [ViewVariables(VVAccess.ReadWrite), DataField("ruptureSound")]
-        public SoundSpecifier RuptureSound = new SoundPathSpecifier("/Audio/Effects/spray.ogg");
-
-        [ViewVariables(VVAccess.ReadWrite), DataField("connectSound")]
-        public SoundSpecifier? ConnectSound =
-            new SoundPathSpecifier("/Audio/Effects/internals.ogg")
-            {
-                Params = AudioParams.Default.WithVolume(5f),
-            };
-
-        [ViewVariables(VVAccess.ReadWrite), DataField("disconnectSound")]
-        public SoundSpecifier? DisconnectSound;
-
-        // Cancel toggles sounds if we re-toggle again.
-
-        public EntityUid? ConnectStream;
-        public EntityUid? DisconnectStream;
-
-        [DataField("air"), ViewVariables(VVAccess.ReadWrite)]
-        public GasMixture Air { get; set; } = new();
-
-        /// <summary>
-        ///     Pressure at which tank should be considered 'low' such as for internals.
-        /// </summary>
-        [DataField("tankLowPressure"), ViewVariables(VVAccess.ReadWrite)]
-        public float TankLowPressure = DefaultLowPressure;
-
-        /// <summary>
-        ///     Distributed pressure.
-        /// </summary>
-        [DataField("outputPressure"), ViewVariables(VVAccess.ReadWrite)]
-        public float OutputPressure = DefaultOutputPressure;
-
-        /// <summary>
-        ///     The maximum allowed output pressure.
-        /// </summary>
-        [DataField("maxOutputPressure"), ViewVariables(VVAccess.ReadWrite)]
-        public float MaxOutputPressure = 3 * DefaultOutputPressure;
-
-        /// <summary>
-        ///     Tank is connected to internals.
-        /// </summary>
-        [ViewVariables]
-        public bool IsConnected => User != null;
-
-        [ViewVariables]
-        public EntityUid? User;
-
-        /// <summary>
-        ///     True if this entity was recently moved out of a container. This might have been a hand -> inventory
-        ///     transfer, or it might have been the user dropping the tank. This indicates the tank needs to be checked.
-        /// </summary>
-        [ViewVariables]
-        public bool CheckUser;
-
-        /// <summary>
-        ///     Pressure at which tanks start leaking.
-        /// </summary>
-        [DataField("tankLeakPressure"), ViewVariables(VVAccess.ReadWrite)]
-        public float TankLeakPressure = 30 * Atmospherics.OneAtmosphere;
-
-        /// <summary>
-        ///     Pressure at which tank spills all contents into atmosphere.
-        /// </summary>
-        [DataField("tankRupturePressure"), ViewVariables(VVAccess.ReadWrite)]
-        public float TankRupturePressure = 40 * Atmospherics.OneAtmosphere;
-
-        /// <summary>
-        ///     Base 3x3 explosion.
-        /// </summary>
-        [DataField("tankFragmentPressure"), ViewVariables(VVAccess.ReadWrite)]
-        public float TankFragmentPressure = 50 * Atmospherics.OneAtmosphere;
-
-        /// <summary>
-        ///     Increases explosion for each scale kPa above threshold.
-        /// </summary>
-        [DataField("tankFragmentScale"), ViewVariables(VVAccess.ReadWrite)]
-        public float TankFragmentScale = 2 * Atmospherics.OneAtmosphere;
-
-        [DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-        public string ToggleAction = "ActionToggleInternals";
-
-        [DataField("toggleActionEntity")] public EntityUid? ToggleActionEntity;
-
-        /// <summary>
-        ///     Valve to release gas from tank
-        /// </summary>
-        [DataField("isValveOpen"), ViewVariables(VVAccess.ReadWrite)]
-        public bool IsValveOpen = false;
-
-        /// <summary>
-        ///     Gas release rate in L/s
-        /// </summary>
-        [DataField("valveOutputRate"), ViewVariables(VVAccess.ReadWrite)]
-        public float ValveOutputRate = 100f;
-
-        [DataField("valveSound"), ViewVariables(VVAccess.ReadWrite)]
-        public SoundSpecifier ValveSound =
-            new SoundCollectionSpecifier("valveSqueak")
-            {
-                Params = AudioParams.Default.WithVolume(-5f),
-            };
-    }
-}
index 8c786e5b216e9b26b533578a739887fa2a980f1f..532ba04d29592fedab79485f3a23f086ceef9182 100644 (file)
@@ -18,6 +18,7 @@ using Robust.Shared.Timing;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Content.Shared.DeviceNetwork.Components;
+using Content.Shared.NodeContainer;
 
 namespace Content.Server.Atmos.Consoles;
 
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BreathTool.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BreathTool.cs
deleted file mode 100644 (file)
index 327804f..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-using Content.Server.Atmos.Components;
-using Content.Server.Body.Components;
-
-namespace Content.Server.Atmos.EntitySystems;
-
-public sealed partial class AtmosphereSystem
-{
-    private void InitializeBreathTool()
-    {
-        SubscribeLocalEvent<BreathToolComponent, ComponentShutdown>(OnBreathToolShutdown);
-    }
-
-    private void OnBreathToolShutdown(Entity<BreathToolComponent> entity, ref ComponentShutdown args)
-    {
-        DisconnectInternals(entity);
-    }
-
-    public void DisconnectInternals(Entity<BreathToolComponent> entity)
-    {
-        var old = entity.Comp.ConnectedInternalsEntity;
-        entity.Comp.ConnectedInternalsEntity = null;
-
-        if (TryComp<InternalsComponent>(old, out var internalsComponent))
-        {
-            _internals.DisconnectBreathTool((old.Value, internalsComponent), entity.Owner);
-        }
-
-        entity.Comp.IsFunctional = false;
-    }
-}
index b11eb5dc3e670ae4adbdb8d471275b1ff3f32d0b..72b6e9d74594f07b9689c1a0e968a44c980fbf40 100644 (file)
@@ -44,8 +44,6 @@ public sealed partial class AtmosphereSystem
 
     private void OnGridAtmosphereInit(EntityUid uid, GridAtmosphereComponent component, ComponentInit args)
     {
-        base.Initialize();
-
         EnsureComp<GasTileOverlayComponent>(uid);
         foreach (var tile in component.Tiles.Values)
         {
index f27f7411bf443937a295054152104eb797e062cd..0f5bc1af0423fd7a3b8de7e11b996ee9ba68b169 100644 (file)
@@ -28,7 +28,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
     [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
     [Dependency] private readonly IAdminLogManager _adminLog = default!;
     [Dependency] private readonly EntityLookupSystem _lookup = default!;
-    [Dependency] private readonly InternalsSystem _internals = default!;
     [Dependency] private readonly SharedContainerSystem _containers = default!;
     [Dependency] private readonly SharedPhysicsSystem _physics = default!;
     [Dependency] private readonly GasTileOverlaySystem _gasTileOverlaySystem = default!;
@@ -56,7 +55,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
 
         UpdatesAfter.Add(typeof(NodeGroupSystem));
 
-        InitializeBreathTool();
         InitializeGases();
         InitializeCommands();
         InitializeCVars();
index cc541e35e9a16399ae81f28026153b6874209c28..c3f934549b0ebd4c0efd639d17ae0a4e07806afd 100644 (file)
@@ -7,6 +7,7 @@ using Content.Shared.Atmos;
 using Content.Shared.Atmos.Components;
 using Content.Shared.Interaction;
 using Content.Shared.Interaction.Events;
+using Content.Shared.NodeContainer;
 using JetBrains.Annotations;
 using Robust.Server.GameObjects;
 using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
index 4d409a708229b488f8a4b9bb5ebd1afb36fd78b9..08177a7d9599b7982f011a59aad2acd488dffc08 100644 (file)
@@ -1,21 +1,13 @@
-using Content.Server.Atmos.Components;
-using Content.Server.Body.Components;
-using Content.Server.Body.Systems;
 using Content.Server.Cargo.Systems;
 using Content.Server.Explosion.EntitySystems;
-using Content.Shared.UserInterface;
-using Content.Shared.Actions;
 using Content.Shared.Atmos;
 using Content.Shared.Atmos.Components;
-using Content.Shared.Examine;
+using Content.Shared.Atmos.EntitySystems;
 using Content.Shared.Throwing;
-using Content.Shared.Toggleable;
-using Content.Shared.Verbs;
 using JetBrains.Annotations;
 using Robust.Server.GameObjects;
 using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
-using Robust.Shared.Containers;
 using Robust.Shared.Random;
 using Robust.Shared.Configuration;
 using Content.Shared.CCVar;
@@ -23,14 +15,11 @@ using Content.Shared.CCVar;
 namespace Content.Server.Atmos.EntitySystems
 {
     [UsedImplicitly]
-    public sealed class GasTankSystem : EntitySystem
+    public sealed class GasTankSystem : SharedGasTankSystem
     {
         [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
         [Dependency] private readonly ExplosionSystem _explosions = default!;
-        [Dependency] private readonly InternalsSystem _internals = default!;
         [Dependency] private readonly SharedAudioSystem _audioSys = default!;
-        [Dependency] private readonly SharedContainerSystem _containers = default!;
-        [Dependency] private readonly SharedActionsSystem _actions = default!;
         [Dependency] private readonly UserInterfaceSystem _ui = default!;
         [Dependency] private readonly IRobustRandom _random = default!;
         [Dependency] private readonly ThrowingSystem _throwing = default!;
@@ -44,17 +33,9 @@ namespace Content.Server.Atmos.EntitySystems
         public override void Initialize()
         {
             base.Initialize();
-            SubscribeLocalEvent<GasTankComponent, ComponentShutdown>(OnGasShutdown);
-            SubscribeLocalEvent<GasTankComponent, BeforeActivatableUIOpenEvent>(BeforeUiOpen);
-            SubscribeLocalEvent<GasTankComponent, GetItemActionsEvent>(OnGetActions);
-            SubscribeLocalEvent<GasTankComponent, ExaminedEvent>(OnExamined);
-            SubscribeLocalEvent<GasTankComponent, ToggleActionEvent>(OnActionToggle);
             SubscribeLocalEvent<GasTankComponent, EntParentChangedMessage>(OnParentChange);
-            SubscribeLocalEvent<GasTankComponent, GasTankSetPressureMessage>(OnGasTankSetPressure);
-            SubscribeLocalEvent<GasTankComponent, GasTankToggleInternalsMessage>(OnGasTankToggleInternals);
             SubscribeLocalEvent<GasTankComponent, GasAnalyzerScanEvent>(OnAnalyzed);
             SubscribeLocalEvent<GasTankComponent, PriceCalculationEvent>(OnGasTankPrice);
-            SubscribeLocalEvent<GasTankComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerb);
             Subs.CVar(_cfg, CCVars.AtmosTankFragment, UpdateMaxRange, true);
         }
 
@@ -63,44 +44,16 @@ namespace Content.Server.Atmos.EntitySystems
             _maxExplosionRange = value;
         }
 
-        private void OnGasShutdown(Entity<GasTankComponent> gasTank, ref ComponentShutdown args)
-        {
-            DisconnectFromInternals(gasTank);
-        }
-
-        private void OnGasTankToggleInternals(Entity<GasTankComponent> ent, ref GasTankToggleInternalsMessage args)
-        {
-            ToggleInternals(ent);
-        }
-
-        private void OnGasTankSetPressure(Entity<GasTankComponent> ent, ref GasTankSetPressureMessage args)
-        {
-            var pressure = Math.Clamp(args.Pressure, 0f, ent.Comp.MaxOutputPressure);
-
-            ent.Comp.OutputPressure = pressure;
-
-            UpdateUserInterface(ent, true);
-        }
-
-        public void UpdateUserInterface(Entity<GasTankComponent> ent, bool initialUpdate = false)
+        public override void UpdateUserInterface(Entity<GasTankComponent> ent)
         {
             var (owner, component) = ent;
             _ui.SetUiState(owner, SharedGasTankUiKey.Key,
                 new GasTankBoundUserInterfaceState
                 {
                     TankPressure = component.Air?.Pressure ?? 0,
-                    OutputPressure = initialUpdate ? component.OutputPressure : null,
-                    InternalsConnected = component.IsConnected,
-                    CanConnectInternals = CanConnectToInternals(ent)
                 });
         }
 
-        private void BeforeUiOpen(Entity<GasTankComponent> ent, ref BeforeActivatableUIOpenEvent args)
-        {
-            // Only initial update includes output pressure information, to avoid overwriting client-input as the updates come in.
-            UpdateUserInterface(ent, true);
-        }
-
         private void OnParentChange(EntityUid uid, GasTankComponent component, ref EntParentChangedMessage args)
         {
             // When an item is moved from hands -> pockets, the container removal briefly dumps the item on the floor.
@@ -109,30 +62,6 @@ namespace Content.Server.Atmos.EntitySystems
             component.CheckUser = true;
         }
 
-        private void OnGetActions(EntityUid uid, GasTankComponent component, GetItemActionsEvent args)
-        {
-            args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
-        }
-
-        private void OnExamined(EntityUid uid, GasTankComponent component, ExaminedEvent args)
-        {
-            using var _ = args.PushGroup(nameof(GasTankComponent));
-            if (args.IsInDetailsRange)
-                args.PushMarkup(Loc.GetString("comp-gas-tank-examine", ("pressure", Math.Round(component.Air?.Pressure ?? 0))));
-            if (component.IsConnected)
-                args.PushMarkup(Loc.GetString("comp-gas-tank-connected"));
-            args.PushMarkup(Loc.GetString(component.IsValveOpen ? "comp-gas-tank-examine-open-valve" : "comp-gas-tank-examine-closed-valve"));
-        }
-
-        private void OnActionToggle(Entity<GasTankComponent> gasTank, ref ToggleActionEvent args)
-        {
-            if (args.Handled)
-                return;
-
-            ToggleInternals(gasTank);
-            args.Handled = true;
-        }
-
         public override void Update(float frameTime)
         {
             base.Update(frameTime);
@@ -167,8 +96,10 @@ namespace Content.Server.Atmos.EntitySystems
                 {
                     _atmosphereSystem.React(comp.Air, comp);
                 }
+
                 CheckStatus(gasTank);
-                if (_ui.IsUiOpen(uid, SharedGasTankUiKey.Key))
+
+                if ((comp.IsConnected || comp.IsValveOpen) && _ui.IsUiOpen(uid, SharedGasTankUiKey.Key))
                 {
                     UpdateUserInterface(gasTank);
                 }
@@ -190,18 +121,6 @@ namespace Content.Server.Atmos.EntitySystems
                 _audioSys.PlayPvs(gasTank.Comp.RuptureSound, gasTank);
         }
 
-        private void ToggleInternals(Entity<GasTankComponent> ent)
-        {
-            if (ent.Comp.IsConnected)
-            {
-                DisconnectFromInternals(ent);
-            }
-            else
-            {
-                ConnectToInternals(ent);
-            }
-        }
-
         public GasMixture? RemoveAir(Entity<GasTankComponent> gasTank, float amount)
         {
             var gas = gasTank.Comp.Air?.Remove(amount);
@@ -227,95 +146,6 @@ namespace Content.Server.Atmos.EntitySystems
             return air;
         }
 
-        public bool CanConnectToInternals(Entity<GasTankComponent> ent)
-        {
-            TryGetInternalsComp(ent, out _, out var internalsComp, ent.Comp.User);
-            return internalsComp != null && internalsComp.BreathTools.Count != 0 && !ent.Comp.IsValveOpen;
-        }
-
-        public void ConnectToInternals(Entity<GasTankComponent> ent)
-        {
-            var (owner, component) = ent;
-            if (component.IsConnected || !CanConnectToInternals(ent))
-                return;
-
-            TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, ent.Comp.User);
-            if (internalsUid == null || internalsComp == null)
-                return;
-
-            if (_internals.TryConnectTank((internalsUid.Value, internalsComp), owner))
-                component.User = internalsUid.Value;
-
-            _actions.SetToggled(component.ToggleActionEntity, component.IsConnected);
-
-            // Couldn't toggle!
-            if (!component.IsConnected)
-                return;
-
-            component.ConnectStream = _audioSys.Stop(component.ConnectStream);
-            component.ConnectStream = _audioSys.PlayPvs(component.ConnectSound, owner)?.Entity;
-
-            UpdateUserInterface(ent);
-        }
-
-        public void DisconnectFromInternals(Entity<GasTankComponent> ent)
-        {
-            var (owner, component) = ent;
-
-            if (component.User == null)
-                return;
-
-            TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, component.User);
-            component.User = null;
-
-            _actions.SetToggled(component.ToggleActionEntity, false);
-
-            if (internalsUid != null && internalsComp != null)
-                _internals.DisconnectTank((internalsUid.Value, internalsComp));
-            component.DisconnectStream = _audioSys.Stop(component.DisconnectStream);
-            component.DisconnectStream = _audioSys.PlayPvs(component.DisconnectSound, owner)?.Entity;
-
-            UpdateUserInterface(ent);
-        }
-
-        /// <summary>
-        /// Tries to retrieve the internals component of either the gas tank's user,
-        /// or the gas tank's... containing container
-        /// </summary>
-        /// <param name="user">The user of the gas tank</param>
-        /// <returns>True if internals comp isn't null, false if it is null</returns>
-        private bool TryGetInternalsComp(Entity<GasTankComponent> ent, out EntityUid? internalsUid, out InternalsComponent? internalsComp, EntityUid? user = null)
-        {
-            internalsUid = default;
-            internalsComp = default;
-
-            // If the gas tank doesn't exist for whatever reason, don't even bother
-            if (TerminatingOrDeleted(ent.Owner))
-                return false;
-
-            user ??= ent.Comp.User;
-            // Check if the gas tank's user actually has the component that allows them to use a gas tank and mask
-            if (TryComp<InternalsComponent>(user, out var userInternalsComp) && userInternalsComp != null)
-            {
-                internalsUid = user;
-                internalsComp = userInternalsComp;
-                return true;
-            }
-
-            // Yeah I have no clue what this actually does, I appreciate the lack of comments on the original function
-            if (_containers.TryGetContainingContainer((ent.Owner, Transform(ent.Owner)), out var container) && container != null)
-            {
-                if (TryComp<InternalsComponent>(container.Owner, out var containerInternalsComp) && containerInternalsComp != null)
-                {
-                    internalsUid = container.Owner;
-                    internalsComp = containerInternalsComp;
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
         public void AssumeAir(Entity<GasTankComponent> ent, GasMixture giver)
         {
             _atmosphereSystem.Merge(ent.Comp.Air, giver);
@@ -404,21 +234,5 @@ namespace Content.Server.Atmos.EntitySystems
         {
             args.Price += _atmosphereSystem.GetPrice(component.Air);
         }
-
-        private void OnGetAlternativeVerb(EntityUid uid, GasTankComponent component, GetVerbsEvent<AlternativeVerb> args)
-        {
-            if (!args.CanAccess || !args.CanInteract || args.Hands == null)
-                return;
-            args.Verbs.Add(new AlternativeVerb()
-            {
-                Text = component.IsValveOpen ? Loc.GetString("comp-gas-tank-close-valve") : Loc.GetString("comp-gas-tank-open-valve"),
-                Act = () =>
-                {
-                    component.IsValveOpen = !component.IsValveOpen;
-                    _audioSys.PlayPvs(component.ValveSound, uid);
-                },
-                Disabled = component.IsConnected,
-            });
-        }
     }
 }
index c2ff87ca79c3af3a03d5a262bad559b3d27b8701..f64aff47f4e896c997bebab7220e5cb8564781f0 100644 (file)
@@ -5,6 +5,7 @@ using Content.Server.NodeContainer.Nodes;
 using Content.Server.Popups;
 using Content.Shared.Atmos;
 using Content.Shared.Construction.Components;
+using Content.Shared.NodeContainer;
 using JetBrains.Annotations;
 using Robust.Server.GameObjects;
 using Robust.Shared.Map.Components;
diff --git a/Content.Server/Atmos/IGasMixtureHolder.cs b/Content.Server/Atmos/IGasMixtureHolder.cs
deleted file mode 100644 (file)
index 65d7ba6..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-using Content.Shared.Atmos;
-
-namespace Content.Server.Atmos
-{
-    public interface IGasMixtureHolder
-    {
-        public GasMixture Air { get; set; }
-    }
-}
index 10049e273bc4198866bd374c571049524f89f1e5..53cc35463ce567861c9aa412b90f846f63cc9abd 100644 (file)
@@ -3,6 +3,7 @@ using Content.Server.NodeContainer.EntitySystems;
 using Content.Server.NodeContainer.Nodes;
 using Content.Shared.Atmos;
 using Content.Shared.Atmos.Components;
+using Content.Shared.NodeContainer;
 using Robust.Shared.Map.Components;
 
 namespace Content.Server.Atmos.Piping.EntitySystems;
index bc925e433ca61a4a4a8917cadc14b8be2c5220bd..25439736c7c99eb97f438e50ec7400da19c808ec 100644 (file)
@@ -7,6 +7,7 @@ using Content.Server.Popups;
 using Content.Shared.Atmos;
 using Content.Shared.Construction.Components;
 using Content.Shared.Destructible;
+using Content.Shared.NodeContainer;
 using Content.Shared.Popups;
 using JetBrains.Annotations;
 
diff --git a/Content.Server/Atmos/Piping/Unary/Components/GasCanisterComponent.cs b/Content.Server/Atmos/Piping/Unary/Components/GasCanisterComponent.cs
deleted file mode 100644 (file)
index afbfb91..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-using Content.Shared.Atmos;
-using Content.Shared.Containers.ItemSlots;
-using Content.Shared.Guidebook;
-using Robust.Shared.Audio;
-
-namespace Content.Server.Atmos.Piping.Unary.Components
-{
-    [RegisterComponent]
-    public sealed partial class GasCanisterComponent : Component, IGasMixtureHolder
-    {
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("port")]
-        public string PortName { get; set; } = "port";
-
-        /// <summary>
-        ///     Container name for the gas tank holder.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("container")]
-        public string ContainerName { get; set; } = "tank_slot";
-
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField]
-        public ItemSlot GasTankSlot = new();
-
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("gasMixture")]
-        public GasMixture Air { get; set; } = new();
-
-        /// <summary>
-        ///     Last recorded pressure, for appearance-updating purposes.
-        /// </summary>
-        public float LastPressure { get; set; } = 0f;
-
-        /// <summary>
-        ///     Minimum release pressure possible for the release valve.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("minReleasePressure")]
-        public float MinReleasePressure { get; set; } = Atmospherics.OneAtmosphere / 10;
-
-        /// <summary>
-        ///     Maximum release pressure possible for the release valve.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("maxReleasePressure")]
-        public float MaxReleasePressure { get; set; } = Atmospherics.OneAtmosphere * 10;
-
-        /// <summary>
-        ///     Valve release pressure.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("releasePressure")]
-        public float ReleasePressure { get; set; } = Atmospherics.OneAtmosphere;
-
-        /// <summary>
-        ///     Whether the release valve is open on the canister.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("releaseValve")]
-        public bool ReleaseValve { get; set; } = false;
-
-        [DataField("accessDeniedSound")]
-        public SoundSpecifier AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
-
-        #region GuidebookData
-
-        [GuidebookData]
-        public float Volume => Air.Volume;
-
-        #endregion
-    }
-}
index 292b6d94f82669193eb4f3916846cb521e99417e..a8f505ca5d7a70bc8d31b09c21916a5f9aec1676 100644 (file)
@@ -1,55 +1,32 @@
-using Content.Server.Administration.Logs;
-using Content.Server.Atmos.Components;
 using Content.Server.Atmos.EntitySystems;
 using Content.Server.Atmos.Piping.Components;
-using Content.Server.Atmos.Piping.Unary.Components;
 using Content.Server.Cargo.Systems;
-using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.EntitySystems;
 using Content.Server.NodeContainer.NodeGroups;
 using Content.Server.NodeContainer.Nodes;
-using Content.Server.Popups;
 using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
 using Content.Shared.Atmos.Piping.Binary.Components;
-using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Atmos.Piping.Unary.Systems;
 using Content.Shared.Database;
-using Content.Shared.Interaction;
-using Content.Shared.Lock;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Containers;
-using Robust.Shared.Player;
+using Content.Shared.NodeContainer;
+using GasCanisterComponent = Content.Shared.Atmos.Piping.Unary.Components.GasCanisterComponent;
 
 namespace Content.Server.Atmos.Piping.Unary.EntitySystems;
 
-public sealed class GasCanisterSystem : EntitySystem
+public sealed class GasCanisterSystem : SharedGasCanisterSystem
 {
     [Dependency] private readonly AtmosphereSystem _atmos = default!;
-    [Dependency] private readonly IAdminLogManager _adminLogger = default!;
     [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-    [Dependency] private readonly SharedAudioSystem _audio = default!;
-    [Dependency] private readonly PopupSystem _popup = default!;
-    [Dependency] private readonly UserInterfaceSystem _ui = default!;
     [Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
-    [Dependency] private readonly ItemSlotsSystem _slots = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<GasCanisterComponent, ComponentStartup>(OnCanisterStartup);
         SubscribeLocalEvent<GasCanisterComponent, AtmosDeviceUpdateEvent>(OnCanisterUpdated);
-        SubscribeLocalEvent<GasCanisterComponent, ActivateInWorldEvent>(OnCanisterActivate, after: new[] { typeof(LockSystem) });
-        SubscribeLocalEvent<GasCanisterComponent, InteractHandEvent>(OnCanisterInteractHand);
-        SubscribeLocalEvent<GasCanisterComponent, ItemSlotInsertAttemptEvent>(OnCanisterInsertAttempt);
-        SubscribeLocalEvent<GasCanisterComponent, EntInsertedIntoContainerMessage>(OnCanisterContainerInserted);
-        SubscribeLocalEvent<GasCanisterComponent, EntRemovedFromContainerMessage>(OnCanisterContainerRemoved);
         SubscribeLocalEvent<GasCanisterComponent, PriceCalculationEvent>(CalculateCanisterPrice);
         SubscribeLocalEvent<GasCanisterComponent, GasAnalyzerScanEvent>(OnAnalyzed);
-        // Bound UI subscriptions
-        SubscribeLocalEvent<GasCanisterComponent, GasCanisterHoldingTankEjectMessage>(OnHoldingTankEjectMessage);
-        SubscribeLocalEvent<GasCanisterComponent, GasCanisterChangeReleasePressureMessage>(OnCanisterChangeReleasePressure);
-        SubscribeLocalEvent<GasCanisterComponent, GasCanisterChangeReleaseValveMessage>(OnCanisterChangeReleaseValve);
     }
 
     /// <summary>
@@ -65,24 +42,16 @@ public sealed class GasCanisterSystem : EntitySystem
         if (environment is not null)
             _atmos.Merge(environment, canister.Air);
 
-        _adminLogger.Add(LogType.CanisterPurged, LogImpact.Medium, $"Canister {ToPrettyString(uid):canister} purged its contents of {canister.Air:gas} into the environment.");
+        AdminLogger.Add(LogType.CanisterPurged, LogImpact.Medium, $"Canister {ToPrettyString(uid):canister} purged its contents of {canister.Air:gas} into the environment.");
         canister.Air.Clear();
     }
 
-    private void OnCanisterStartup(EntityUid uid, GasCanisterComponent comp, ComponentStartup args)
-    {
-        // Ensure container
-        _slots.AddItemSlot(uid, comp.ContainerName, comp.GasTankSlot);
-    }
-
-    private void DirtyUI(EntityUid uid,
-        GasCanisterComponent? canister = null, NodeContainerComponent? nodeContainer = null)
+    protected override void DirtyUI(EntityUid uid, GasCanisterComponent? canister = null, NodeContainerComponent? nodeContainer = null)
     {
         if (!Resolve(uid, ref canister, ref nodeContainer))
             return;
 
         var portStatus = false;
-        string? tankLabel = null;
         var tankPressure = 0f;
 
         if (_nodeContainer.TryGetNode(nodeContainer, canister.PortName, out PipeNode? portNode) && portNode.NodeGroup?.Nodes.Count > 1)
@@ -92,62 +61,11 @@ public sealed class GasCanisterSystem : EntitySystem
         {
             var tank = canister.GasTankSlot.Item.Value;
             var tankComponent = Comp<GasTankComponent>(tank);
-            tankLabel = Name(tank);
             tankPressure = tankComponent.Air.Pressure;
         }
 
-        _ui.SetUiState(uid, GasCanisterUiKey.Key,
-            new GasCanisterBoundUserInterfaceState(Name(uid),
-                canister.Air.Pressure, portStatus, tankLabel, tankPressure, canister.ReleasePressure,
-                canister.ReleaseValve, canister.MinReleasePressure, canister.MaxReleasePressure));
-    }
-
-    private void OnHoldingTankEjectMessage(EntityUid uid, GasCanisterComponent canister, GasCanisterHoldingTankEjectMessage args)
-    {
-        if (canister.GasTankSlot.Item == null)
-            return;
-
-        var item = canister.GasTankSlot.Item;
-        _slots.TryEjectToHands(uid, canister.GasTankSlot, args.Actor);
-
-        if (canister.ReleaseValve)
-        {
-            _adminLogger.Add(LogType.CanisterTankEjected, LogImpact.High, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister} while the valve was open, releasing [{GetContainedGasesString((uid, canister))}] to atmosphere");
-        }
-        else
-        {
-            _adminLogger.Add(LogType.CanisterTankEjected, LogImpact.Medium, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister}");
-        }
-    }
-
-    private void OnCanisterChangeReleasePressure(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleasePressureMessage args)
-    {
-        var pressure = Math.Clamp(args.Pressure, canister.MinReleasePressure, canister.MaxReleasePressure);
-
-        _adminLogger.Add(LogType.CanisterPressure, LogImpact.Medium, $"{ToPrettyString(args.Actor):player} set the release pressure on {ToPrettyString(uid):canister} to {args.Pressure}");
-
-        canister.ReleasePressure = pressure;
-        DirtyUI(uid, canister);
-    }
-
-    private void OnCanisterChangeReleaseValve(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleaseValveMessage args)
-    {
-        // filling a jetpack with plasma is less important than filling a room with it
-        var hasItem = canister.GasTankSlot.HasItem;
-        var impact = hasItem ? LogImpact.Medium : LogImpact.High;
-
-        _adminLogger.Add(
-            LogType.CanisterValve,
-            impact,
-            $"{ToPrettyString(args.Actor):player} {(args.Valve ? "opened" : "closed")} the valve on {ToPrettyString(uid):canister} to {(hasItem ? "inserted tank" : "environment")} while it contained [{GetContainedGasesString((uid, canister))}]");
-
-        canister.ReleaseValve = args.Valve;
-        DirtyUI(uid, canister);
-    }
-
-    private static string GetContainedGasesString(Entity<GasCanisterComponent> canister)
-    {
-        return string.Join(", ", canister.Comp.Air);
+        UI.SetUiState(uid, GasCanisterUiKey.Key,
+            new GasCanisterBoundUserInterfaceState(canister.Air.Pressure, portStatus, tankPressure));
     }
 
     private void OnCanisterUpdated(EntityUid uid, GasCanisterComponent canister, ref AtmosDeviceUpdateEvent args)
@@ -207,76 +125,6 @@ public sealed class GasCanisterSystem : EntitySystem
         }
     }
 
-    private void OnCanisterActivate(EntityUid uid, GasCanisterComponent component, ActivateInWorldEvent args)
-    {
-        if (!args.Complex)
-            return;
-
-        if (!TryComp<ActorComponent>(args.User, out var actor))
-            return;
-
-        if (CheckLocked(uid, component, args.User))
-            return;
-
-        // Needs to be here so the locked check still happens if the canister
-        // is locked and you don't have permissions
-        if (args.Handled)
-            return;
-
-        _ui.OpenUi(uid, GasCanisterUiKey.Key, actor.PlayerSession);
-        args.Handled = true;
-    }
-
-    private void OnCanisterInteractHand(EntityUid uid, GasCanisterComponent component, InteractHandEvent args)
-    {
-        if (!TryComp<ActorComponent>(args.User, out var actor))
-            return;
-
-        if (CheckLocked(uid, component, args.User))
-            return;
-
-        _ui.OpenUi(uid, GasCanisterUiKey.Key, actor.PlayerSession);
-        args.Handled = true;
-    }
-
-    private void OnCanisterInsertAttempt(EntityUid uid, GasCanisterComponent component, ref ItemSlotInsertAttemptEvent args)
-    {
-        if (args.Slot.ID != component.ContainerName || args.User == null)
-            return;
-
-        if (!TryComp<GasTankComponent>(args.Item, out var gasTank) || gasTank.IsValveOpen)
-        {
-            args.Cancelled = true;
-            return;
-        }
-
-        // Preventing inserting a tank since if its locked you cant remove it.
-        if (!CheckLocked(uid, component, args.User.Value))
-            return;
-
-        args.Cancelled = true;
-    }
-
-    private void OnCanisterContainerInserted(EntityUid uid, GasCanisterComponent component, EntInsertedIntoContainerMessage args)
-    {
-        if (args.Container.ID != component.ContainerName)
-            return;
-
-        DirtyUI(uid, component);
-
-        _appearance.SetData(uid, GasCanisterVisuals.TankInserted, true);
-    }
-
-    private void OnCanisterContainerRemoved(EntityUid uid, GasCanisterComponent component, EntRemovedFromContainerMessage args)
-    {
-        if (args.Container.ID != component.ContainerName)
-            return;
-
-        DirtyUI(uid, component);
-
-        _appearance.SetData(uid, GasCanisterVisuals.TankInserted, false);
-    }
-
     /// <summary>
     /// Mix air from a gas container into a pipe net.
     /// Useful for anything that uses connector ports.
@@ -317,23 +165,4 @@ public sealed class GasCanisterSystem : EntitySystem
             args.GasMixtures.Add((Name(tank), tankComponent.Air));
         }
     }
-
-    /// <summary>
-    /// Check if the canister is locked, playing its sound and popup if so.
-    /// </summary>
-    /// <returns>
-    /// True if locked, false otherwise.
-    /// </returns>
-    private bool CheckLocked(EntityUid uid, GasCanisterComponent comp, EntityUid user)
-    {
-        if (TryComp<LockComponent>(uid, out var lockComp) && lockComp.Locked)
-        {
-            _popup.PopupEntity(Loc.GetString("gas-canister-popup-denied"), uid, user);
-            _audio.PlayPvs(comp.AccessDeniedSound, uid);
-
-            return true;
-        }
-
-        return false;
-    }
 }
diff --git a/Content.Server/Body/Components/InternalsComponent.cs b/Content.Server/Body/Components/InternalsComponent.cs
deleted file mode 100644 (file)
index a7edab4..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-using Content.Shared.Alert;
-using Robust.Shared.Prototypes;
-
-namespace Content.Server.Body.Components
-{
-    /// <summary>
-    /// Handles hooking up a mask (breathing tool) / gas tank together and allowing the Owner to breathe through it.
-    /// </summary>
-    [RegisterComponent]
-    public sealed partial class InternalsComponent : Component
-    {
-        [ViewVariables]
-        public EntityUid? GasTankEntity;
-
-        [ViewVariables]
-        public HashSet<EntityUid> BreathTools { get; set; } = new();
-
-        /// <summary>
-        /// Toggle Internals delay when the target is not you.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField]
-        public TimeSpan Delay = TimeSpan.FromSeconds(3);
-
-        [DataField]
-        public ProtoId<AlertPrototype> InternalsAlert = "Internals";
-    }
-
-}
index 7e86cb6f07ea8962a391a42661d8be02fcd418c2..29c2c363d29b61f136ef0484d9b033a94f705344 100644 (file)
@@ -1,28 +1,21 @@
-using Content.Server.Atmos.Components;
 using Content.Server.Atmos.EntitySystems;
-using Content.Server.Body.Components;
 using Content.Server.Popups;
 using Content.Shared.Alert;
 using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Systems;
 using Content.Shared.DoAfter;
-using Content.Shared.Hands.Components;
 using Content.Shared.Internals;
 using Content.Shared.Inventory;
 using Content.Shared.Roles;
-using Content.Shared.Verbs;
-using Robust.Shared.Containers;
-using Robust.Shared.Utility;
 
 namespace Content.Server.Body.Systems;
 
-public sealed class InternalsSystem : EntitySystem
+public sealed class InternalsSystem : SharedInternalsSystem
 {
     [Dependency] private readonly AlertsSystem _alerts = default!;
-    [Dependency] private readonly AtmosphereSystem _atmos = default!;
-    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
     [Dependency] private readonly GasTankSystem _gasTank = default!;
-    [Dependency] private readonly InventorySystem _inventory = default!;
-    [Dependency] private readonly PopupSystem _popupSystem = default!;
     [Dependency] private readonly RespiratorSystem _respirator = default!;
 
     private EntityQuery<InternalsComponent> _internalsQuery;
@@ -34,12 +27,6 @@ public sealed class InternalsSystem : EntitySystem
         _internalsQuery = GetEntityQuery<InternalsComponent>();
 
         SubscribeLocalEvent<InternalsComponent, InhaleLocationEvent>(OnInhaleLocation);
-        SubscribeLocalEvent<InternalsComponent, ComponentStartup>(OnInternalsStartup);
-        SubscribeLocalEvent<InternalsComponent, ComponentShutdown>(OnInternalsShutdown);
-        SubscribeLocalEvent<InternalsComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs);
-        SubscribeLocalEvent<InternalsComponent, InternalsDoAfterEvent>(OnDoAfter);
-        SubscribeLocalEvent<InternalsComponent, ToggleInternalsAlertEvent>(OnToggleInternalsAlert);
-
         SubscribeLocalEvent<InternalsComponent, StartingGearEquippedEvent>(OnStartingGear);
     }
 
@@ -66,120 +53,6 @@ public sealed class InternalsSystem : EntitySystem
         ToggleInternals(uid, uid, force: false, component);
     }
 
-    private void OnGetInteractionVerbs(
-        Entity<InternalsComponent> ent,
-        ref GetVerbsEvent<InteractionVerb> args)
-    {
-        if (!args.CanAccess || !args.CanInteract || args.Hands is null)
-            return;
-
-        if (!AreInternalsWorking(ent) && ent.Comp.BreathTools.Count == 0)
-            return;
-
-        var user = args.User;
-
-        InteractionVerb verb = new()
-        {
-            Act = () =>
-            {
-                ToggleInternals(ent, user, force: false, ent);
-            },
-            Message = Loc.GetString("action-description-internals-toggle"),
-            Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/dot.svg.192dpi.png")),
-            Text = Loc.GetString("action-name-internals-toggle"),
-        };
-
-        args.Verbs.Add(verb);
-    }
-
-    public void ToggleInternals(
-        EntityUid uid,
-        EntityUid user,
-        bool force,
-        InternalsComponent? internals = null)
-    {
-        if (!Resolve(uid, ref internals, logMissing: false))
-            return;
-
-        // Toggle off if they're on
-        if (AreInternalsWorking(internals))
-        {
-            if (force)
-            {
-                DisconnectTank((uid, internals));
-                return;
-            }
-
-            StartToggleInternalsDoAfter(user, (uid, internals));
-            return;
-        }
-
-        // If they're not on then check if we have a mask to use
-        if (internals.BreathTools.Count == 0)
-        {
-            _popupSystem.PopupEntity(Loc.GetString("internals-no-breath-tool"), uid, user);
-            return;
-        }
-
-        var tank = FindBestGasTank(uid);
-
-        if (tank is null)
-        {
-            _popupSystem.PopupEntity(Loc.GetString("internals-no-tank"), uid, user);
-            return;
-        }
-
-        if (!force)
-        {
-            StartToggleInternalsDoAfter(user, (uid, internals));
-            return;
-        }
-
-        _gasTank.ConnectToInternals(tank.Value);
-    }
-
-    private void StartToggleInternalsDoAfter(EntityUid user, Entity<InternalsComponent> targetEnt)
-    {
-        // Is the target not you? If yes, use a do-after to give them time to respond.
-        var isUser = user == targetEnt.Owner;
-        var delay = !isUser ? targetEnt.Comp.Delay : TimeSpan.Zero;
-
-        _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, delay, new InternalsDoAfterEvent(), targetEnt, target: targetEnt)
-        {
-            BreakOnDamage = true,
-            BreakOnMove =  true,
-            MovementThreshold = 0.1f,
-        });
-    }
-
-    private void OnDoAfter(Entity<InternalsComponent> ent, ref InternalsDoAfterEvent args)
-    {
-        if (args.Cancelled || args.Handled)
-            return;
-
-        ToggleInternals(ent, args.User, force: true, ent);
-
-        args.Handled = true;
-    }
-
-    private void OnToggleInternalsAlert(Entity<InternalsComponent> ent, ref ToggleInternalsAlertEvent args)
-    {
-        if (args.Handled)
-            return;
-        ToggleInternals(ent, ent, false, internals: ent.Comp);
-        args.Handled = true;
-    }
-
-    private void OnInternalsStartup(Entity<InternalsComponent> ent, ref ComponentStartup args)
-    {
-        _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
-    }
-
-    private void OnInternalsShutdown(Entity<InternalsComponent> ent, ref ComponentShutdown args)
-    {
-        _alerts.ClearAlert(ent, ent.Comp.InternalsAlert);
-    }
-
     private void OnInhaleLocation(Entity<InternalsComponent> ent, ref InhaleLocationEvent args)
     {
         if (AreInternalsWorking(ent))
@@ -190,110 +63,4 @@ public sealed class InternalsSystem : EntitySystem
             _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
         }
     }
-    public void DisconnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
-    {
-        ent.Comp.BreathTools.Remove(toolEntity);
-
-        if (TryComp(toolEntity, out BreathToolComponent? breathTool))
-            _atmos.DisconnectInternals((toolEntity, breathTool));
-
-        if (ent.Comp.BreathTools.Count == 0)
-            DisconnectTank(ent);
-
-        _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
-    }
-
-    public void ConnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
-    {
-        if (!ent.Comp.BreathTools.Add(toolEntity))
-            return;
-
-        _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
-    }
-
-    public void DisconnectTank(Entity<InternalsComponent> ent)
-    {
-        if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
-            _gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank));
-
-        ent.Comp.GasTankEntity = null;
-        _alerts.ShowAlert(ent.Owner, ent.Comp.InternalsAlert, GetSeverity(ent.Comp));
-    }
-
-    public bool TryConnectTank(Entity<InternalsComponent> ent, EntityUid tankEntity)
-    {
-        if (ent.Comp.BreathTools.Count == 0)
-            return false;
-
-        if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
-            _gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank));
-
-        ent.Comp.GasTankEntity = tankEntity;
-        _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
-        return true;
-    }
-
-    public bool AreInternalsWorking(EntityUid uid, InternalsComponent? component = null)
-    {
-        return Resolve(uid, ref component, logMissing: false)
-            && AreInternalsWorking(component);
-    }
-
-    public bool AreInternalsWorking(InternalsComponent component)
-    {
-        return TryComp(component.BreathTools.FirstOrNull(), out BreathToolComponent? breathTool)
-            && breathTool.IsFunctional
-            && HasComp<GasTankComponent>(component.GasTankEntity);
-    }
-
-    private short GetSeverity(InternalsComponent component)
-    {
-        if (component.BreathTools.Count == 0 || !AreInternalsWorking(component))
-            return 2;
-
-        // If pressure in the tank is below low pressure threshold, flash warning on internals UI
-        if (TryComp<GasTankComponent>(component.GasTankEntity, out var gasTank)
-            && gasTank.IsLowPressure)
-        {
-            return 0;
-        }
-
-        return 1;
-    }
-
-    public Entity<GasTankComponent>? FindBestGasTank(
-        Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user)
-    {
-        // TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses
-        // Prioritise
-        // 1. back equipped tanks
-        // 2. exo-slot tanks
-        // 3. in-hand tanks
-        // 4. pocket/belt tanks
-
-        if (!Resolve(user, ref user.Comp2, ref user.Comp3))
-            return null;
-
-        if (_inventory.TryGetSlotEntity(user, "back", out var backEntity, user.Comp2, user.Comp3) &&
-            TryComp<GasTankComponent>(backEntity, out var backGasTank) &&
-            _gasTank.CanConnectToInternals((backEntity.Value, backGasTank)))
-        {
-            return (backEntity.Value, backGasTank);
-        }
-
-        if (_inventory.TryGetSlotEntity(user, "suitstorage", out var entity, user.Comp2, user.Comp3) &&
-            TryComp<GasTankComponent>(entity, out var gasTank) &&
-            _gasTank.CanConnectToInternals((entity.Value, gasTank)))
-        {
-            return (entity.Value, gasTank);
-        }
-
-        foreach (var item in _inventory.GetHandOrInventoryEntities((user.Owner, user.Comp1, user.Comp2)))
-        {
-            if (TryComp(item, out gasTank) && _gasTank.CanConnectToInternals((item, gasTank)))
-                return (item, gasTank);
-        }
-
-        return null;
-    }
 }
index 82ec490a4ae60cdee230872c244d520d0c7f91b0..273a8466ca0b52fc781061cfcd9e042ea4b53c50 100644 (file)
@@ -1,4 +1,3 @@
-using Content.Server.Atmos.Components;
 using Content.Server.Atmos.EntitySystems;
 using Content.Server.Body.Components;
 using Content.Shared.Chemistry.EntitySystems;
@@ -6,6 +5,8 @@ using Content.Shared.Atmos;
 using Content.Shared.Chemistry.Components;
 using Content.Shared.Clothing;
 using Content.Shared.Inventory.Events;
+using BreathToolComponent = Content.Shared.Atmos.Components.BreathToolComponent;
+using InternalsComponent = Content.Shared.Body.Components.InternalsComponent;
 
 namespace Content.Server.Body.Systems;
 
@@ -23,7 +24,6 @@ public sealed class LungSystem : EntitySystem
         SubscribeLocalEvent<LungComponent, ComponentInit>(OnComponentInit);
         SubscribeLocalEvent<BreathToolComponent, GotEquippedEvent>(OnGotEquipped);
         SubscribeLocalEvent<BreathToolComponent, GotUnequippedEvent>(OnGotUnequipped);
-        SubscribeLocalEvent<BreathToolComponent, ItemMaskToggledEvent>(OnMaskToggled);
     }
 
     private void OnGotUnequipped(Entity<BreathToolComponent> ent, ref GotUnequippedEvent args)
@@ -38,8 +38,6 @@ public sealed class LungSystem : EntitySystem
             return;
         }
 
-        ent.Comp.IsFunctional = true;
-
         if (TryComp(args.Equipee, out InternalsComponent? internals))
         {
             ent.Comp.ConnectedInternalsEntity = args.Equipee;
@@ -56,24 +54,6 @@ public sealed class LungSystem : EntitySystem
         }
     }
 
-    private void OnMaskToggled(Entity<BreathToolComponent> ent, ref ItemMaskToggledEvent args)
-    {
-        if (args.Mask.Comp.IsToggled)
-        {
-            _atmos.DisconnectInternals(ent);
-        }
-        else
-        {
-            ent.Comp.IsFunctional = true;
-
-            if (TryComp(args.Wearer, out InternalsComponent? internals))
-            {
-                ent.Comp.ConnectedInternalsEntity = args.Wearer;
-                _internals.ConnectBreathTool((args.Wearer.Value, internals), ent);
-            }
-        }
-    }
-
     public void GasToReagent(EntityUid uid, LungComponent lung)
     {
         if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution))
index 9975f04bbb38cdce17793103852b172b607141c1..b03cceda0f3bb0090856a49959fed1dc7433381b 100644 (file)
@@ -5,6 +5,7 @@ using Content.Server.Power.Nodes;
 using Content.Server.Power.NodeGroups;
 using Content.Shared.Examine;
 using Content.Shared.Interaction;
+using Content.Shared.NodeContainer;
 using Content.Shared.Popups;
 using Content.Shared.Power.Generator;
 using Content.Shared.Timing;
index 4f021e7b71e40d696f30b545871671b6a6bd8f03..ad13f9211d7a7261f6716e59b1685ad2519d87b6 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.DeviceNetwork.Systems;
 using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
 
 namespace Content.Server.DeviceNetwork.Components
 {
index 798a9b540ec5494d59114490b676cf39ee812e0f..288732bb0ae47964c23e785c43b043ba66ac01f7 100644 (file)
@@ -5,6 +5,7 @@ using JetBrains.Annotations;
 using Content.Server.Power.EntitySystems;
 using Content.Server.Power.Nodes;
 using Content.Shared.DeviceNetwork.Events;
+using Content.Shared.NodeContainer;
 
 namespace Content.Server.DeviceNetwork.Systems
 {
index c8e437d3532539f881c76dab193370546982099c..ddf09fcce7418226aa06e1fe11b54150dc0c2e0a 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.EntitySystems;
 using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
 using Robust.Shared.Map.Components;
 
 namespace Content.Server.Electrocution
index 0059a8b42729c144aa0d7242569dcc583a5bbe4e..957f881bed4962773a833f6ea69af4f86f3e34e8 100644 (file)
@@ -17,6 +17,8 @@ using Content.Shared.Interaction;
 using Content.Shared.Inventory;
 using Content.Shared.Jittering;
 using Content.Shared.Maps;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
 using Content.Shared.Popups;
 using Content.Shared.Speech.EntitySystems;
 using Content.Shared.StatusEffect;
index 546336890fde2f0d9c74316766e806b22aaa094d..2ca807ae728856454fc3c3a10d2468f391f66c50 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.Atmos.Components;
 using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos.Components;
 using Content.Shared.Movement.Components;
 using Content.Shared.Movement.Systems;
 using Robust.Shared.Collections;
index c0603949be551cdeb06251d7d59fbc286731d8ec..ac818e08dcfef567140ecb9f3dd1022ac1b45309 100644 (file)
@@ -2,6 +2,8 @@ using System.Diagnostics.CodeAnalysis;
 using Content.Server.NodeContainer.NodeGroups;
 using Content.Server.NodeContainer.Nodes;
 using Content.Shared.Examine;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
 using JetBrains.Annotations;
 
 namespace Content.Server.NodeContainer.EntitySystems
index 62806fe84fb0b6ac15262202224392cc350215da..7b55e20f8a27a5013657b6dd16b1e02f8ff68147 100644 (file)
@@ -5,6 +5,7 @@ using Content.Server.NodeContainer.NodeGroups;
 using Content.Server.NodeContainer.Nodes;
 using Content.Shared.Administration;
 using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
 using JetBrains.Annotations;
 using Robust.Server.Player;
 using Robust.Shared.Enums;
diff --git a/Content.Server/NodeContainer/NodeContainerComponent.cs b/Content.Server/NodeContainer/NodeContainerComponent.cs
deleted file mode 100644 (file)
index de4586d..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using Content.Server.NodeContainer.Nodes;
-
-namespace Content.Server.NodeContainer
-{
-    /// <summary>
-    ///     Creates and maintains a set of <see cref="Node"/>s.
-    /// </summary>
-    [RegisterComponent]
-    public sealed partial class NodeContainerComponent : Component
-    {
-        //HACK: THIS BEING readOnly IS A FILTHY HACK AND I HATE IT --moony
-        [DataField("nodes", readOnly: true)] public Dictionary<string, Node> Nodes { get; private set; } = new();
-
-        [DataField("examinable")] public bool Examinable = false;
-    }
-}
index 1481d7b1c2afd3a7e184dfbfc6d190f3868cface..052d158c1a3d8d6de49ca2fbf33bdad9f18c0baf 100644 (file)
@@ -1,40 +1,12 @@
 using System.Linq;
 using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
 using Robust.Shared.Map;
 using Robust.Shared.Utility;
 
 namespace Content.Server.NodeContainer.NodeGroups
 {
-    /// <summary>
-    ///     Maintains a collection of <see cref="Node"/>s, and performs operations requiring a list of
-    ///     all connected <see cref="Node"/>s.
-    /// </summary>
-    public interface INodeGroup
-    {
-        bool Remaking { get; }
-
-        /// <summary>
-        ///     The list of nodes currently in this group.
-        /// </summary>
-        IReadOnlyList<Node> Nodes { get; }
-
-        void Create(NodeGroupID groupId);
-
-        void Initialize(Node sourceNode, IEntityManager entMan);
-
-        void RemoveNode(Node node);
-
-        void LoadNodes(List<Node> groupNodes);
-
-        // In theory, the SS13 curse ensures this method will never be called.
-        void AfterRemake(IEnumerable<IGrouping<INodeGroup?, Node>> newGroups);
-
-        /// <summary>
-        ///     Return any additional data to display for the node-visualizer debug overlay.
-        /// </summary>
-        string? GetDebugData();
-    }
-
     [NodeGroup(NodeGroupID.Default, NodeGroupID.WireNet)]
     [Virtual]
     public class BaseNodeGroup : INodeGroup
index 9b4979a524269b26cd2966aac0219a55d9ec454e..9200be790f420c9526813df57d0b7302814a794a 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.NodeContainer.NodeGroups;
 using JetBrains.Annotations;
 
 namespace Content.Server.NodeContainer.NodeGroups
index abebfd1a90fb5dbdb0473f6829f49be3e300d79a..71ca91f0f98c7af7b2c26e704ebede77c7b4a84d 100644 (file)
@@ -1,5 +1,5 @@
 using System.Reflection;
-using Content.Server.Power.Generation.Teg;
+using Content.Shared.NodeContainer.NodeGroups;
 using Robust.Shared.Reflection;
 
 namespace Content.Server.NodeContainer.NodeGroups
@@ -51,22 +51,4 @@ namespace Content.Server.NodeContainer.NodeGroups
             return instance;
         }
     }
-
-    public enum NodeGroupID : byte
-    {
-        Default,
-        HVPower,
-        MVPower,
-        Apc,
-        AMEngine,
-        Pipe,
-        WireNet,
-
-        /// <summary>
-        /// Group used by the TEG.
-        /// </summary>
-        /// <seealso cref="TegSystem"/>
-        /// <seealso cref="TegNodeGroup"/>
-        Teg,
-    }
 }
index e905a6e78e87bb8ac6c4d16b89f4fb8cbaa15327..3c8e65ca91f4ec5e9280f983e80a59dfa33b4b81 100644 (file)
@@ -3,6 +3,8 @@ using Content.Server.Atmos;
 using Content.Server.Atmos.EntitySystems;
 using Content.Server.NodeContainer.Nodes;
 using Content.Shared.Atmos;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
 using Robust.Shared.Utility;
 
 namespace Content.Server.NodeContainer.NodeGroups
index 6df534b285cf5598658e6c00e06801deab3cdde1..d719ccbff079e9a1cc4be7b5ef236cc24b40b188 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.NodeContainer;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 
index 38b6dbdf192c9fc1d8866861fefe802198861a13..b2b51367a119608f54fcba031d0ec7eda8999af2 100644 (file)
@@ -1,3 +1,5 @@
+using Content.Shared.NodeContainer;
+
 namespace Content.Server.NodeContainer.Nodes
 {
     /// <summary>
diff --git a/Content.Server/NodeContainer/Nodes/Node.cs b/Content.Server/NodeContainer/Nodes/Node.cs
deleted file mode 100644 (file)
index fe9299e..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-using Content.Server.NodeContainer.EntitySystems;
-using Content.Server.NodeContainer.NodeGroups;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-
-namespace Content.Server.NodeContainer.Nodes
-{
-    /// <summary>
-    ///     Organizes themselves into distinct <see cref="INodeGroup"/>s with other <see cref="Node"/>s
-    ///     that they can "reach" and have the same <see cref="Node.NodeGroupID"/>.
-    /// </summary>
-    [ImplicitDataDefinitionForInheritors]
-    public abstract partial class Node
-    {
-        /// <summary>
-        ///     An ID used as a criteria for combining into groups. Determines which <see cref="INodeGroup"/>
-        ///     implementation is used as a group, detailed in <see cref="INodeGroupFactory"/>.
-        /// </summary>
-        [DataField("nodeGroupID")]
-        public NodeGroupID NodeGroupID { get; private set; } = NodeGroupID.Default;
-
-        /// <summary>
-        ///     The node group this node is a part of.
-        /// </summary>
-        [ViewVariables] public INodeGroup? NodeGroup;
-
-        /// <summary>
-        ///     The entity that owns this node via its <see cref="NodeContainerComponent"/>.
-        /// </summary>
-        [ViewVariables] public EntityUid Owner { get; private set; } = default!;
-
-        /// <summary>
-        ///     If this node should be considered for connection by other nodes.
-        /// </summary>
-        public virtual bool Connectable(IEntityManager entMan, TransformComponent? xform = null)
-        {
-            if (Deleting)
-                return false;
-
-            if (entMan.IsQueuedForDeletion(Owner))
-                return false;
-
-            if (!NeedAnchored)
-                return true;
-
-            xform ??= entMan.GetComponent<TransformComponent>(Owner);
-            return xform.Anchored;
-        }
-
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("needAnchored")]
-        public bool NeedAnchored { get; private set; } = true;
-
-        public virtual void OnAnchorStateChanged(IEntityManager entityManager, bool anchored) { }
-
-        /// <summary>
-        ///    Prevents a node from being used by other nodes while midway through removal.
-        /// </summary>
-        public bool Deleting;
-
-        /// <summary>
-        ///     All compatible nodes that are reachable by this node.
-        ///     Effectively, active connections out of this node.
-        /// </summary>
-        public readonly HashSet<Node> ReachableNodes = new();
-
-        internal int FloodGen;
-        internal int UndirectGen;
-        internal bool FlaggedForFlood;
-        internal int NetId;
-
-        /// <summary>
-        ///     Name of this node on the owning <see cref="NodeContainerComponent"/>.
-        /// </summary>
-        public string Name = default!;
-
-        /// <summary>
-        ///     Invoked when the owning <see cref="NodeContainerComponent"/> is initialized.
-        /// </summary>
-        /// <param name="owner">The owning entity.</param>
-        public virtual void Initialize(EntityUid owner, IEntityManager entMan)
-        {
-            Owner = owner;
-        }
-
-        /// <summary>
-        ///     How this node will attempt to find other reachable <see cref="Node"/>s to group with.
-        ///     Returns a set of <see cref="Node"/>s to consider grouping with. Should not return this current <see cref="Node"/>.
-        /// </summary>
-        /// <remarks>
-        /// <para>
-        /// The set of nodes returned can be asymmetrical
-        /// (meaning that it can return other nodes whose <see cref="GetReachableNodes"/> does not return this node).
-        /// If this is used, creation of a new node may not correctly merge networks unless both sides
-        /// of this asymmetric relation are made to manually update with <see cref="NodeGroupSystem.QueueReflood"/>.
-        /// </para>
-        /// </remarks>
-        public abstract IEnumerable<Node> GetReachableNodes(TransformComponent xform,
-            EntityQuery<NodeContainerComponent> nodeQuery,
-            EntityQuery<TransformComponent> xformQuery,
-            MapGridComponent? grid,
-            IEntityManager entMan);
-    }
-}
index ad6d59dbafd1b8d315717dba5f2c0e0d37c34fd3..c2345fff760884e95d120df8cc3b9fa74551efde 100644 (file)
@@ -1,4 +1,5 @@
 using System.Diagnostics.CodeAnalysis;
+using Content.Shared.NodeContainer;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 
index 31ee5712493d698427b79ba6f8670714af04ec70..d30d3b1777beca09d91bf81e72b538e0ac488c5e 100644 (file)
@@ -2,6 +2,8 @@ using Content.Server.Atmos;
 using Content.Server.NodeContainer.EntitySystems;
 using Content.Server.NodeContainer.NodeGroups;
 using Content.Shared.Atmos;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Utility;
index 69d85b42248744f9ec73a426a6b5fda96f755a08..04e0dc0ab7a992adcfada77ff2515e1e6c76ff79 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.NodeContainer;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 
index 287cd6b3b52a292fc39792ba7e8c0d1c21125770..427288ee502557394293cdad34a78b695ab79eab 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.NodeContainer;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 
index 7882522d30ba27d3217d2c35c41aac2a61efb112..3cf25472f0af95a9954277c0d4098b52762cc887 100644 (file)
@@ -3,6 +3,7 @@ using Content.Server.Atmos.EntitySystems;
 using Content.Server.Storage.EntitySystems;
 using Content.Server.Stunnable;
 using Content.Server.Weapons.Ranged.Systems;
+using Content.Shared.Atmos.Components;
 using Content.Shared.Containers.ItemSlots;
 using Content.Shared.Interaction;
 using Content.Shared.PneumaticCannon;
index f2a1372dbc99b2c487dda7a03359f45362bc046b..b22950c1a1ed88129ef383da829319bac472ed21 100644 (file)
@@ -2,6 +2,8 @@
 using System.Linq;
 using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.NodeGroups;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
 
 namespace Content.Server.Power.Components
 {
index c8f90d9ad5fb9a198a1b72308142c56395dff95d..f01710fbe1359728e884b93e73aa964e067ee07d 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.NodeGroups;
 using Content.Server.Power.EntitySystems;
+using Content.Shared.NodeContainer;
 using Content.Shared.Power;
 
 namespace Content.Server.Power.Components;
index 761f274ba5fd479ae2310a7a1449446e284c6b99..485f0c00b4731b0f36de1161df935bca112cd48f 100644 (file)
@@ -4,6 +4,7 @@ using Content.Server.Power.NodeGroups;
 using Content.Server.Tools;
 using Content.Shared.Examine;
 using Content.Shared.Interaction;
+using Content.Shared.NodeContainer;
 using Content.Shared.Tools.Systems;
 using Content.Shared.Verbs;
 using JetBrains.Annotations;
index 0fc641ed2804bf53e3d445098e889d3d5f5c1fed..7b41af66b983ab851bcd688c76a8bdac245e8aa9 100644 (file)
@@ -14,6 +14,7 @@ using Robust.Server.GameObjects;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Utility;
 using System.Linq;
+using Content.Shared.NodeContainer;
 
 namespace Content.Server.Power.EntitySystems;
 
index 3c937f8f71d03dcd09b810297451f889ab16d70d..92a353ccb11def805db4252d548e006025660beb 100644 (file)
@@ -2,6 +2,8 @@
 using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.NodeGroups;
 using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Utility;
 
index 04f876c2c283158ec21f7467a538a8a9a1b6111f..77848d5c796344649ea3d5a02e4781f7af9304a5 100644 (file)
@@ -11,6 +11,7 @@ using Content.Shared.Atmos;
 using Content.Shared.DeviceNetwork;
 using Content.Shared.DeviceNetwork.Events;
 using Content.Shared.Examine;
+using Content.Shared.NodeContainer;
 using Content.Shared.Power;
 using Content.Shared.Power.EntitySystems;
 using Content.Shared.Power.Generation.Teg;
index 61892f23f8683c54f5d2789f554a9f0d62849a72..7b7d7ce2569b198b8e219c6f6cc355040ad483e1 100644 (file)
@@ -3,6 +3,7 @@ using Content.Server.NodeContainer.EntitySystems;
 using Content.Server.Popups;
 using Content.Server.Power.Components;
 using Content.Server.Power.Nodes;
+using Content.Shared.NodeContainer;
 using Content.Shared.Power.Generator;
 using Content.Shared.Timing;
 using Content.Shared.Verbs;
index 8c0b89b5071dec4e81826cd1a936c0596d630e16..60fad61fcca40514a6f7a37ca074245b4f87e584 100644 (file)
@@ -3,6 +3,8 @@ using Content.Server.NodeContainer.NodeGroups;
 using Content.Server.NodeContainer.Nodes;
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
 using JetBrains.Annotations;
 
 namespace Content.Server.Power.NodeGroups
index d70fbceed39e364bbe5bb8c3f167b1fa5d963c60..6f5673796a2129a3f42c5db61ee6c8d9c1906f74 100644 (file)
@@ -1,6 +1,8 @@
 using Content.Server.NodeContainer.NodeGroups;
 using Content.Server.NodeContainer.Nodes;
 using Content.Server.Power.Components;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
 
 namespace Content.Server.Power.NodeGroups
 {
index 16287022253193227ebd9a8de8efe472b7b169bc..15ed2630d4f5f805cfdaefedab5102914f3e3cbe 100644 (file)
@@ -2,6 +2,7 @@
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Server.Power.Pow3r;
+using Content.Shared.NodeContainer;
 using Robust.Shared.Utility;
 
 namespace Content.Server.Power.NodeGroups;
index edbc5661e248410ba9b60d50b1a92127a8ddf18f..ab84623be4d481385cce57481b31ade6ccfe6efd 100644 (file)
@@ -5,6 +5,8 @@ using Content.Server.Power.EntitySystems;
 using JetBrains.Annotations;
 using Robust.Shared.Utility;
 using System.Linq;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
 
 namespace Content.Server.Power.NodeGroups
 {
index d7914f2b2eadeeae0acaeac80b16cd4796008538..4089cd5657911d1f3f732b4474f0a1b67a1f934d 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.EntitySystems;
 using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
 using Robust.Shared.Map.Components;
 
 namespace Content.Server.Power.Nodes
index 4f2dbd42dffc99402533105121f13aab7bc93ce0..b8c404e8e24d504aceb1d242d9092731ff7c190f 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 
index bb44e8625677bf50cc1909f6011231ac3617ebb1..8988a9950b0d05d3fcf7c2e54c85a22a1a2a5c81 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 
index bbce1aff744d3966a8a78a07722ea6cbc0d82f87..f71f1c4aa632374e72ecacd1af6e8302428c9641 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 
index aaea23ddd566941bf457ba8f6efa7205cd12cb36..1ff28719fc13a704c7587c9276b7103b79897bff 100644 (file)
@@ -1,6 +1,7 @@
 using System.Linq;
 using System.Threading.Tasks;
 using Content.Server.NodeContainer;
+using Content.Shared.NodeContainer;
 using Content.Shared.Procedural;
 using Content.Shared.Procedural.PostGeneration;
 using Robust.Shared.Random;
index 0a778f588033bbad6073377f95a0edef0a6465cd..2fcfcd8cbbf3e83beb38cfb80e64b9b3bd0d78cb 100644 (file)
@@ -4,6 +4,8 @@ using Content.Server.Atmos.Piping.EntitySystems;
 using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.NodeGroups;
 using Content.Shared.Administration;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
 using Robust.Shared.Console;
 
 namespace Content.Server.Sandbox.Commands
index bdd775c79273c5ca2640a742fce4753a23a7069d..f8027b2f14155c51622b767734711237113ff0f1 100644 (file)
@@ -7,6 +7,7 @@ using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Server.Singularity.Components;
 using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
 using Content.Shared.Examine;
 using Content.Shared.Interaction;
 using Content.Shared.Radiation.Events;
index c8f2a8e6bcacd09a3bb9c78c2b4ec31150bbe480..429c0670ad9cbd5fecccf9251ae4be2e19a4b286 100644 (file)
@@ -8,8 +8,8 @@ namespace Content.Shared.Alert;
 
 public abstract class AlertsSystem : EntitySystem
 {
-    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
 
     private FrozenDictionary<ProtoId<AlertPrototype>, AlertPrototype> _typeToAlert = default!;
 
@@ -328,7 +328,15 @@ public abstract class AlertsSystem : EntitySystem
             return;
         }
 
-        ActivateAlert(player.Value, alert);
+        if (ActivateAlert(player.Value, alert) && _timing.IsFirstTimePredicted)
+        {
+            HandledAlert();
+        }
+    }
+
+    protected virtual void HandledAlert()
+    {
+
     }
 
     public bool ActivateAlert(EntityUid user, AlertPrototype alert)
diff --git a/Content.Shared/Atmos/Components/BreathToolComponent.cs b/Content.Shared/Atmos/Components/BreathToolComponent.cs
new file mode 100644 (file)
index 0000000..b511258
--- /dev/null
@@ -0,0 +1,28 @@
+using Content.Shared.Body.Components;
+using Content.Shared.Inventory;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Atmos.Components;
+
+/// <summary>
+/// Gas masks or the likes; used by <see cref="InternalsComponent"/> for breathing.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[ComponentProtoName("BreathMask")]
+public sealed partial class BreathToolComponent : Component
+{
+    /// <summary>
+    /// Tool is functional only in allowed slots
+    /// </summary>
+    [DataField]
+    public SlotFlags AllowedSlots = SlotFlags.MASK | SlotFlags.HEAD;
+
+    [ViewVariables]
+    public bool IsFunctional => ConnectedInternalsEntity != null;
+
+    /// <summary>
+    /// Entity that the breath tool is currently connected to.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public EntityUid? ConnectedInternalsEntity;
+}
diff --git a/Content.Shared/Atmos/Components/GasTankComponent.cs b/Content.Shared/Atmos/Components/GasTankComponent.cs
new file mode 100644 (file)
index 0000000..4214913
--- /dev/null
@@ -0,0 +1,120 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Atmos.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+public sealed partial class GasTankComponent : Component, IGasMixtureHolder
+{
+    public const float MaxExplosionRange = 26f;
+    private const float DefaultLowPressure = 0f;
+    private const float DefaultOutputPressure = Atmospherics.OneAtmosphere;
+
+    public int Integrity = 3;
+    public bool IsLowPressure => Air.Pressure <= TankLowPressure;
+
+    [DataField]
+    public SoundSpecifier RuptureSound = new SoundPathSpecifier("/Audio/Effects/spray.ogg");
+
+    [DataField]
+    public SoundSpecifier? ConnectSound =
+        new SoundPathSpecifier("/Audio/Effects/internals.ogg")
+        {
+            Params = AudioParams.Default.WithVolume(5f),
+        };
+
+    [DataField]
+    public SoundSpecifier? DisconnectSound;
+
+    // Cancel toggles sounds if we re-toggle again.
+
+    public EntityUid? ConnectStream;
+    public EntityUid? DisconnectStream;
+
+    [DataField]
+    public GasMixture Air { get; set; } = new();
+
+    /// <summary>
+    ///     Pressure at which tank should be considered 'low' such as for internals.
+    /// </summary>
+    [DataField]
+    public float TankLowPressure = DefaultLowPressure;
+
+    /// <summary>
+    ///     Distributed pressure.
+    /// </summary>
+    [DataField]
+    public float OutputPressure = DefaultOutputPressure;
+
+    /// <summary>
+    ///     The maximum allowed output pressure.
+    /// </summary>
+    [DataField]
+    public float MaxOutputPressure = 3 * DefaultOutputPressure;
+
+    /// <summary>
+    ///     Tank is connected to internals.
+    /// </summary>
+    [ViewVariables]
+    public bool IsConnected => User != null;
+
+    [DataField, AutoNetworkedField]
+    public EntityUid? User;
+
+    /// <summary>
+    ///     True if this entity was recently moved out of a container. This might have been a hand -> inventory
+    ///     transfer, or it might have been the user dropping the tank. This indicates the tank needs to be checked.
+    /// </summary>
+    [ViewVariables]
+    public bool CheckUser;
+
+    /// <summary>
+    ///     Pressure at which tanks start leaking.
+    /// </summary>
+    [DataField]
+    public float TankLeakPressure = 30 * Atmospherics.OneAtmosphere;
+
+    /// <summary>
+    ///     Pressure at which tank spills all contents into atmosphere.
+    /// </summary>
+    [DataField]
+    public float TankRupturePressure = 40 * Atmospherics.OneAtmosphere;
+
+    /// <summary>
+    ///     Base 3x3 explosion.
+    /// </summary>
+    [DataField]
+    public float TankFragmentPressure = 50 * Atmospherics.OneAtmosphere;
+
+    /// <summary>
+    ///     Increases explosion for each scale kPa above threshold.
+    /// </summary>
+    [DataField]
+    public float TankFragmentScale = 2 * Atmospherics.OneAtmosphere;
+
+    [DataField]
+    public EntProtoId ToggleAction = "ActionToggleInternals";
+
+    [DataField, AutoNetworkedField]
+    public EntityUid? ToggleActionEntity;
+
+    /// <summary>
+    ///     Valve to release gas from tank
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool IsValveOpen;
+
+    /// <summary>
+    ///     Gas release rate in L/s
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float ValveOutputRate = 100f;
+
+    [DataField]
+    public SoundSpecifier ValveSound =
+        new SoundCollectionSpecifier("valveSqueak")
+        {
+            Params = AudioParams.Default.WithVolume(-5f),
+        };
+}
index f6751a3ebc10074a0451c2ca62fcd82d63a4460a..536d040766b6c77b464261d5d03e67f5ab888090 100644 (file)
@@ -1,31 +1,24 @@
 using Robust.Shared.Serialization;
 
-namespace Content.Shared.Atmos.Components
-{
-    [Serializable, NetSerializable]
-    public enum SharedGasTankUiKey
-    {
-        Key
-    }
+namespace Content.Shared.Atmos.Components;
 
-    [Serializable, NetSerializable]
-    public sealed class GasTankToggleInternalsMessage : BoundUserInterfaceMessage
-    {
-    }
+[Serializable, NetSerializable]
+public enum SharedGasTankUiKey : byte
+{
+    Key
+}
 
-    [Serializable, NetSerializable]
-    public sealed class GasTankSetPressureMessage : BoundUserInterfaceMessage
-    {
-        public float Pressure { get; set; }
-    }
+[Serializable, NetSerializable]
+public sealed class GasTankToggleInternalsMessage : BoundUserInterfaceMessage;
 
-    [Serializable, NetSerializable]
-    public sealed class GasTankBoundUserInterfaceState : BoundUserInterfaceState
-    {
-        public float TankPressure { get; set; }
-        public float? OutputPressure { get; set; }
-        public bool InternalsConnected { get; set; }
-        public bool CanConnectInternals { get; set; }
+[Serializable, NetSerializable]
+public sealed class GasTankSetPressureMessage : BoundUserInterfaceMessage
+{
+    public float Pressure;
+}
 
-    }
+[Serializable, NetSerializable]
+public sealed class GasTankBoundUserInterfaceState : BoundUserInterfaceState
+{
+    public float TankPressure;
 }
diff --git a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.BreathTool.cs b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.BreathTool.cs
new file mode 100644 (file)
index 0000000..26b4d19
--- /dev/null
@@ -0,0 +1,51 @@
+using Content.Shared.Atmos.Components;
+using Content.Shared.Body.Components;
+using Content.Shared.Clothing;
+
+namespace Content.Shared.Atmos.EntitySystems;
+
+public abstract partial class SharedAtmosphereSystem
+{
+    private void InitializeBreathTool()
+    {
+        SubscribeLocalEvent<BreathToolComponent, ComponentShutdown>(OnBreathToolShutdown);
+        SubscribeLocalEvent<BreathToolComponent, ItemMaskToggledEvent>(OnMaskToggled);
+    }
+
+    private void OnBreathToolShutdown(Entity<BreathToolComponent> entity, ref ComponentShutdown args)
+    {
+        DisconnectInternals(entity);
+    }
+
+    public void DisconnectInternals(Entity<BreathToolComponent> entity, bool forced = false)
+    {
+        var old = entity.Comp.ConnectedInternalsEntity;
+
+        if (old == null)
+            return;
+
+        entity.Comp.ConnectedInternalsEntity = null;
+
+        if (_internalsQuery.TryComp(old, out var internalsComponent))
+        {
+            _internals.DisconnectBreathTool((old.Value, internalsComponent), entity.Owner, forced: forced);
+        }
+
+        Dirty(entity);
+    }
+
+    private void OnMaskToggled(Entity<BreathToolComponent> ent, ref ItemMaskToggledEvent args)
+    {
+        if (args.Mask.Comp.IsToggled)
+        {
+            DisconnectInternals(ent, forced: true);
+        }
+        else
+        {
+            if (_internalsQuery.TryComp(args.Wearer, out var internals))
+            {
+                _internals.ConnectBreathTool((args.Wearer.Value, internals), ent);
+            }
+        }
+    }
+}
index ced9cdcfe8b689c2fcdebadf797be29a8b60a563..46989397347f6c72f3f0a5cfdc8ff841f2cfc922 100644 (file)
@@ -1,12 +1,17 @@
+using Content.Shared.Atmos.Components;
 using Content.Shared.Atmos.Prototypes;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Systems;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
 
 namespace Content.Shared.Atmos.EntitySystems
 {
-    public abstract class SharedAtmosphereSystem : EntitySystem
+    public abstract partial class SharedAtmosphereSystem : EntitySystem
     {
         [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+        [Dependency] private readonly SharedInternalsSystem _internals = default!;
+
+        private EntityQuery<InternalsComponent> _internalsQuery;
 
         protected readonly GasPrototype[] GasPrototypes = new GasPrototype[Atmospherics.TotalNumberOfGases];
 
@@ -14,6 +19,10 @@ namespace Content.Shared.Atmos.EntitySystems
         {
             base.Initialize();
 
+            _internalsQuery = GetEntityQuery<InternalsComponent>();
+
+            InitializeBreathTool();
+
             for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
             {
                 GasPrototypes[i] = _prototypeManager.Index<GasPrototype>(i.ToString());
diff --git a/Content.Shared/Atmos/EntitySystems/SharedGasTankSystem.cs b/Content.Shared/Atmos/EntitySystems/SharedGasTankSystem.cs
new file mode 100644 (file)
index 0000000..27c3d16
--- /dev/null
@@ -0,0 +1,229 @@
+using Content.Shared.Actions;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Body.Systems;
+using Content.Shared.Examine;
+using Content.Shared.Timing;
+using Content.Shared.Toggleable;
+using Content.Shared.UserInterface;
+using Content.Shared.Verbs;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+using InternalsComponent = Content.Shared.Body.Components.InternalsComponent;
+
+namespace Content.Shared.Atmos.EntitySystems;
+
+public abstract class SharedGasTankSystem : EntitySystem
+{
+    [Dependency] private   readonly SharedActionsSystem _actions = default!;
+    [Dependency] private   readonly SharedAudioSystem _audio = default!;
+    [Dependency] private   readonly SharedContainerSystem _containers = default!;
+    [Dependency] private   readonly SharedInternalsSystem _internals = default!;
+    [Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
+    [Dependency] private   readonly UseDelaySystem _delay = default!;
+
+    public const string GasTankDelay = "gasTank";
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<GasTankComponent, ComponentShutdown>(OnGasShutdown);
+        SubscribeLocalEvent<GasTankComponent, BeforeActivatableUIOpenEvent>(BeforeUiOpen);
+        SubscribeLocalEvent<GasTankComponent, GetItemActionsEvent>(OnGetActions);
+        SubscribeLocalEvent<GasTankComponent, ExaminedEvent>(OnExamined);
+        SubscribeLocalEvent<GasTankComponent, ToggleActionEvent>(OnActionToggle);
+        SubscribeLocalEvent<GasTankComponent, GasTankSetPressureMessage>(OnGasTankSetPressure);
+        SubscribeLocalEvent<GasTankComponent, GasTankToggleInternalsMessage>(OnGasTankToggleInternals);
+        SubscribeLocalEvent<GasTankComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerb);
+    }
+
+    private void OnGasShutdown(Entity<GasTankComponent> gasTank, ref ComponentShutdown args)
+    {
+        DisconnectFromInternals(gasTank);
+    }
+
+    private void OnGasTankToggleInternals(Entity<GasTankComponent> ent, ref GasTankToggleInternalsMessage args)
+    {
+        ToggleInternals(ent, args.Actor);
+    }
+
+    private void OnGasTankSetPressure(Entity<GasTankComponent> ent, ref GasTankSetPressureMessage args)
+    {
+        var pressure = Math.Clamp(args.Pressure, 0f, ent.Comp.MaxOutputPressure);
+
+        ent.Comp.OutputPressure = pressure;
+        Dirty(ent);
+    }
+
+    public virtual void UpdateUserInterface(Entity<GasTankComponent> ent)
+    {
+
+    }
+
+    private void BeforeUiOpen(Entity<GasTankComponent> ent, ref BeforeActivatableUIOpenEvent args)
+    {
+        UpdateUserInterface(ent);
+    }
+
+    private void OnGetActions(EntityUid uid, GasTankComponent component, GetItemActionsEvent args)
+    {
+        args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
+        Dirty(uid, component);
+    }
+
+    private void OnExamined(EntityUid uid, GasTankComponent component, ExaminedEvent args)
+    {
+        using var _ = args.PushGroup(nameof(GasTankComponent));
+
+        if (args.IsInDetailsRange)
+            args.PushMarkup(Loc.GetString("comp-gas-tank-examine", ("pressure", Math.Round(component.Air?.Pressure ?? 0))));
+
+        if (component.IsConnected)
+            args.PushMarkup(Loc.GetString("comp-gas-tank-connected"));
+
+        args.PushMarkup(Loc.GetString(component.IsValveOpen ? "comp-gas-tank-examine-open-valve" : "comp-gas-tank-examine-closed-valve"));
+    }
+
+    private void OnActionToggle(Entity<GasTankComponent> gasTank, ref ToggleActionEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        ToggleInternals(gasTank, user: args.Performer);
+        args.Handled = true;
+    }
+
+    private void OnGetAlternativeVerb(EntityUid uid, GasTankComponent component, GetVerbsEvent<AlternativeVerb> args)
+    {
+        if (!args.CanAccess || !args.CanInteract || args.Hands == null)
+            return;
+
+        args.Verbs.Add(new AlternativeVerb()
+        {
+            Text = component.IsValveOpen ? Loc.GetString("comp-gas-tank-close-valve") : Loc.GetString("comp-gas-tank-open-valve"),
+            Act = () =>
+            {
+                component.IsValveOpen = !component.IsValveOpen;
+                _audio.PlayPredicted(component.ValveSound, uid, args.User);
+                Dirty(uid, component);
+            },
+            Disabled = component.IsConnected,
+        });
+    }
+
+    public bool CanConnectToInternals(Entity<GasTankComponent> ent)
+    {
+        TryGetInternalsComp(ent, out _, out var internalsComp, ent.Comp.User);
+        return internalsComp != null && internalsComp.BreathTools.Count != 0 && !ent.Comp.IsValveOpen;
+    }
+
+    public bool ConnectToInternals(Entity<GasTankComponent> ent, EntityUid? user = null)
+    {
+        var (owner, component) = ent;
+        if (component.IsConnected || !CanConnectToInternals(ent))
+            return false;
+
+        TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, ent.Comp.User);
+        if (internalsUid == null || internalsComp == null)
+            return false;
+
+        if (!_delay.TryResetDelay(ent.Owner, checkDelayed: true, id: GasTankDelay))
+            return false;
+
+        if (_internals.TryConnectTank((internalsUid.Value, internalsComp), owner))
+            component.User = internalsUid.Value;
+
+        Dirty(ent);
+        _actions.SetToggled(component.ToggleActionEntity, component.IsConnected);
+        _actions.SetCooldown(component.ToggleActionEntity, TimeSpan.FromSeconds(1));
+
+        // Couldn't toggle!
+        if (!component.IsConnected)
+            return false;
+
+        component.ConnectStream = _audio.Stop(component.ConnectStream);
+        component.ConnectStream = _audio.PlayPredicted(component.ConnectSound, owner, user)?.Entity;
+        UpdateUserInterface(ent);
+        return true;
+    }
+
+    /// <summary>
+    /// Tries to retrieve the internals component of either the gas tank's user,
+    /// or the gas tank's... containing container
+    /// </summary>
+    /// <param name="user">The user of the gas tank</param>
+    /// <returns>True if internals comp isn't null, false if it is null</returns>
+    private bool TryGetInternalsComp(Entity<GasTankComponent> ent, out EntityUid? internalsUid, out InternalsComponent? internalsComp, EntityUid? user = null)
+    {
+        internalsUid = default;
+        internalsComp = default;
+
+        // If the gas tank doesn't exist for whatever reason, don't even bother
+        if (TerminatingOrDeleted(ent.Owner))
+            return false;
+
+        user ??= ent.Comp.User;
+        // Check if the gas tank's user actually has the component that allows them to use a gas tank and mask
+        if (TryComp<InternalsComponent>(user, out var userInternalsComp))
+        {
+            internalsUid = user;
+            internalsComp = userInternalsComp;
+            return true;
+        }
+
+        // Yeah I have no clue what this actually does, I appreciate the lack of comments on the original function
+        if (_containers.TryGetContainingContainer((ent.Owner, Transform(ent.Owner)), out var container))
+        {
+            if (TryComp<InternalsComponent>(container.Owner, out var containerInternalsComp))
+            {
+                internalsUid = container.Owner;
+                internalsComp = containerInternalsComp;
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public bool DisconnectFromInternals(Entity<GasTankComponent> ent, EntityUid? user = null, bool forced = false)
+    {
+        var (owner, component) = ent;
+
+        if (component.User == null)
+            return false;
+
+        if (!forced && !_delay.TryResetDelay(ent.Owner, checkDelayed: true, id: GasTankDelay))
+            return false;
+
+        TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, component.User);
+        component.User = null;
+        Dirty(ent);
+
+        _actions.SetToggled(component.ToggleActionEntity, false);
+
+        // I hate this but actions have no easy way to unify this with usedelay.
+        if (!forced && _delay.TryGetDelayInfo(ent.Owner, out var delayInfo, id: GasTankDelay))
+        {
+            _actions.SetCooldown(component.ToggleActionEntity, delayInfo.Length);
+        }
+
+        if (internalsUid != null && internalsComp != null)
+            _internals.DisconnectTank((internalsUid.Value, internalsComp), forced: forced);
+
+        component.DisconnectStream = _audio.Stop(component.DisconnectStream);
+        component.DisconnectStream = _audio.PlayPredicted(component.DisconnectSound, owner, user)?.Entity;
+        UpdateUserInterface(ent);
+        return true;
+    }
+
+    private bool ToggleInternals(Entity<GasTankComponent> ent, EntityUid? user = null)
+    {
+        if (ent.Comp.IsConnected)
+        {
+            return DisconnectFromInternals(ent, user);
+        }
+        else
+        {
+            return ConnectToInternals(ent, user);
+        }
+    }
+}
diff --git a/Content.Shared/Atmos/IGasMixtureHolder.cs b/Content.Shared/Atmos/IGasMixtureHolder.cs
new file mode 100644 (file)
index 0000000..5ad1003
--- /dev/null
@@ -0,0 +1,6 @@
+namespace Content.Shared.Atmos;
+
+public interface IGasMixtureHolder
+{
+    public GasMixture Air { get; set; }
+}
\ No newline at end of file
index 1203639e0928b48b87ad278f94feff5cd7bcd1b0..6ad576fd8ca43d4b95b8cdb0b85b70cc8a671c86 100644 (file)
@@ -7,7 +7,7 @@ namespace Content.Shared.Atmos.Piping.Binary.Components
     /// Useful when there are multiple UI for an object. Here it's future-proofing only.
     /// </summary>
     [Serializable, NetSerializable]
-    public enum GasCanisterUiKey
+    public enum GasCanisterUiKey : byte
     {
         Key,
     }
@@ -32,27 +32,15 @@ namespace Content.Shared.Atmos.Piping.Binary.Components
     [Serializable, NetSerializable]
     public sealed class GasCanisterBoundUserInterfaceState : BoundUserInterfaceState
     {
-        public string CanisterLabel { get; }
         public float CanisterPressure { get; }
         public bool PortStatus { get; }
-        public string? TankLabel { get; }
         public float TankPressure { get; }
-        public float ReleasePressure { get; }
-        public bool ReleaseValve { get; }
-        public float ReleasePressureMin { get; }
-        public float ReleasePressureMax { get; }
 
-        public GasCanisterBoundUserInterfaceState(string canisterLabel, float canisterPressure, bool portStatus, string? tankLabel, float tankPressure, float releasePressure, bool releaseValve, float releaseValveMin, float releaseValveMax)
+        public GasCanisterBoundUserInterfaceState(float canisterPressure, bool portStatus, float tankPressure)
         {
-            CanisterLabel = canisterLabel;
             CanisterPressure = canisterPressure;
             PortStatus = portStatus;
-            TankLabel = tankLabel;
             TankPressure = tankPressure;
-            ReleasePressure = releasePressure;
-            ReleaseValve = releaseValve;
-            ReleasePressureMin = releaseValveMin;
-            ReleasePressureMax = releaseValveMax;
         }
     }
 
diff --git a/Content.Shared/Atmos/Piping/Unary/Components/GasCanisterComponent.cs b/Content.Shared/Atmos/Piping/Unary/Components/GasCanisterComponent.cs
new file mode 100644 (file)
index 0000000..204cbc7
--- /dev/null
@@ -0,0 +1,58 @@
+using Content.Shared.Atmos.Piping.Binary.Components;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Guidebook;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Atmos.Piping.Unary.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class GasCanisterComponent : Component, IGasMixtureHolder
+{
+    [DataField("port")]
+    public string PortName { get; set; } = "port";
+
+    /// <summary>
+    ///     Container name for the gas tank holder.
+    /// </summary>
+    [DataField("container")]
+    public string ContainerName { get; set; } = "tank_slot";
+
+    [DataField]
+    public ItemSlot GasTankSlot = new();
+
+    [DataField("gasMixture")]
+    public GasMixture Air { get; set; } = new();
+
+    /// <summary>
+    ///     Last recorded pressure, for appearance-updating purposes.
+    /// </summary>
+    public float LastPressure = 0f;
+
+    /// <summary>
+    ///     Minimum release pressure possible for the release valve.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float MinReleasePressure = Atmospherics.OneAtmosphere / 10;
+
+    /// <summary>
+    ///     Maximum release pressure possible for the release valve.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float MaxReleasePressure = Atmospherics.OneAtmosphere * 10;
+
+    /// <summary>
+    ///     Valve release pressure.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float ReleasePressure = Atmospherics.OneAtmosphere;
+
+    /// <summary>
+    ///     Whether the release valve is open on the canister.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool ReleaseValve = false;
+
+    [GuidebookData]
+    public float Volume => Air.Volume;
+}
diff --git a/Content.Shared/Atmos/Piping/Unary/Systems/SharedGasCanisterSystem.cs b/Content.Shared/Atmos/Piping/Unary/Systems/SharedGasCanisterSystem.cs
new file mode 100644 (file)
index 0000000..e7a8ed5
--- /dev/null
@@ -0,0 +1,124 @@
+using Content.Shared.Administration.Logs;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Piping.Binary.Components;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Database;
+using Content.Shared.NodeContainer;
+using Robust.Shared.Containers;
+using GasCanisterComponent = Content.Shared.Atmos.Piping.Unary.Components.GasCanisterComponent;
+
+namespace Content.Shared.Atmos.Piping.Unary.Systems;
+
+public abstract class SharedGasCanisterSystem : EntitySystem
+{
+    [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!;
+    [Dependency] private   readonly ItemSlotsSystem _slots = default!;
+    [Dependency] private   readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<GasCanisterComponent, EntInsertedIntoContainerMessage>(OnCanisterContainerModified);
+        SubscribeLocalEvent<GasCanisterComponent, EntRemovedFromContainerMessage>(OnCanisterContainerModified);
+        SubscribeLocalEvent<GasCanisterComponent, ItemSlotInsertAttemptEvent>(OnCanisterInsertAttempt);
+        SubscribeLocalEvent<GasCanisterComponent, ComponentStartup>(OnCanisterStartup);
+
+        // Bound UI subscriptions
+        SubscribeLocalEvent<GasCanisterComponent, GasCanisterHoldingTankEjectMessage>(OnHoldingTankEjectMessage);
+        SubscribeLocalEvent<GasCanisterComponent, GasCanisterChangeReleasePressureMessage>(OnCanisterChangeReleasePressure);
+        SubscribeLocalEvent<GasCanisterComponent, GasCanisterChangeReleaseValveMessage>(OnCanisterChangeReleaseValve);
+    }
+
+    private void OnCanisterStartup(Entity<GasCanisterComponent> ent, ref ComponentStartup args)
+    {
+        // Ensure container
+        _slots.AddItemSlot(ent.Owner, ent.Comp.ContainerName, ent.Comp.GasTankSlot);
+    }
+
+    private void OnCanisterContainerModified(EntityUid uid, GasCanisterComponent component, ContainerModifiedMessage args)
+    {
+        if (args.Container.ID != component.ContainerName)
+            return;
+
+        DirtyUI(uid, component);
+        _appearance.SetData(uid, GasCanisterVisuals.TankInserted, args is EntInsertedIntoContainerMessage);
+    }
+
+    private static string GetContainedGasesString(Entity<GasCanisterComponent> canister)
+    {
+        return string.Join(", ", canister.Comp.Air);
+    }
+
+    private void OnHoldingTankEjectMessage(EntityUid uid, GasCanisterComponent canister, GasCanisterHoldingTankEjectMessage args)
+    {
+        if (canister.GasTankSlot.Item == null)
+            return;
+
+        var item = canister.GasTankSlot.Item;
+        _slots.TryEjectToHands(uid, canister.GasTankSlot, args.Actor, excludeUserAudio: true);
+
+        if (canister.ReleaseValve)
+        {
+            AdminLogger.Add(LogType.CanisterTankEjected, LogImpact.High, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister} while the valve was open, releasing [{GetContainedGasesString((uid, canister))}] to atmosphere");
+        }
+        else
+        {
+            AdminLogger.Add(LogType.CanisterTankEjected, LogImpact.Medium, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister}");
+        }
+
+        if (UI.TryGetUiState<GasCanisterBoundUserInterfaceState>(uid, GasCanisterUiKey.Key, out var lastState))
+        {
+            // We can at least predict 0 pressure for now even without atmos prediction.
+            var newState = new GasCanisterBoundUserInterfaceState(lastState.CanisterPressure, lastState.PortStatus, 0f);
+            UI.SetUiState(uid, GasCanisterUiKey.Key, newState);
+        }
+
+        DirtyUI(uid, canister);
+    }
+
+    private void OnCanisterChangeReleasePressure(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleasePressureMessage args)
+    {
+        var pressure = Math.Clamp(args.Pressure, canister.MinReleasePressure, canister.MaxReleasePressure);
+
+        AdminLogger.Add(LogType.CanisterPressure, LogImpact.Medium, $"{ToPrettyString(args.Actor):player} set the release pressure on {ToPrettyString(uid):canister} to {args.Pressure}");
+
+        canister.ReleasePressure = pressure;
+        Dirty(uid, canister);
+        DirtyUI(uid, canister);
+    }
+
+    private void OnCanisterChangeReleaseValve(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleaseValveMessage args)
+    {
+        // filling a jetpack with plasma is less important than filling a room with it
+        var impact = canister.GasTankSlot.HasItem ? LogImpact.Medium : LogImpact.High;
+
+        var containedGasDict = new Dictionary<Gas, float>();
+        var containedGasArray = Enum.GetValues(typeof(Gas));
+
+        for (var i = 0; i < containedGasArray.Length; i++)
+        {
+            containedGasDict.Add((Gas)i, canister.Air[i]);
+        }
+
+        AdminLogger.Add(LogType.CanisterValve, impact, $"{ToPrettyString(args.Actor):player} set the valve on {ToPrettyString(uid):canister} to {args.Valve:valveState} while it contained [{string.Join(", ", containedGasDict)}]");
+
+        canister.ReleaseValve = args.Valve;
+        Dirty(uid, canister);
+        DirtyUI(uid, canister);
+    }
+
+    private void OnCanisterInsertAttempt(EntityUid uid, GasCanisterComponent component, ref ItemSlotInsertAttemptEvent args)
+    {
+        if (args.Slot.ID != component.ContainerName || args.User == null)
+            return;
+
+        // Could whitelist but we want to check if it's open so.
+        if (!TryComp<GasTankComponent>(args.Item, out var gasTank) || gasTank.IsValveOpen)
+        {
+            args.Cancelled = true;
+        }
+    }
+
+    protected abstract void DirtyUI(EntityUid uid, GasCanisterComponent? component = null, NodeContainerComponent? nodes = null);
+}
diff --git a/Content.Shared/Body/Components/InternalsComponent.cs b/Content.Shared/Body/Components/InternalsComponent.cs
new file mode 100644 (file)
index 0000000..ff9c977
--- /dev/null
@@ -0,0 +1,27 @@
+using Content.Shared.Alert;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Body.Components;
+
+/// <summary>
+/// Handles hooking up a mask (breathing tool) / gas tank together and allowing the Owner to breathe through it.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+public sealed partial class InternalsComponent : Component
+{
+    [DataField, AutoNetworkedField]
+    public EntityUid? GasTankEntity;
+
+    [DataField, AutoNetworkedField]
+    public HashSet<EntityUid> BreathTools = new();
+
+    /// <summary>
+    /// Toggle Internals delay when the target is not you.
+    /// </summary>
+    [DataField]
+    public TimeSpan Delay = TimeSpan.FromSeconds(3);
+
+    [DataField]
+    public ProtoId<AlertPrototype> InternalsAlert = "Internals";
+}
diff --git a/Content.Shared/Body/Systems/SharedInternalsSystem.cs b/Content.Shared/Body/Systems/SharedInternalsSystem.cs
new file mode 100644 (file)
index 0000000..0924d7a
--- /dev/null
@@ -0,0 +1,273 @@
+using Content.Shared.Alert;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.EntitySystems;
+using Content.Shared.Body.Components;
+using Content.Shared.DoAfter;
+using Content.Shared.Hands.Components;
+using Content.Shared.Internals;
+using Content.Shared.Inventory;
+using Content.Shared.Popups;
+using Content.Shared.Verbs;
+using Robust.Shared.Containers;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Body.Systems;
+
+/// <summary>
+/// Handles lung breathing with gas tanks for entities.
+/// </summary>
+public abstract class SharedInternalsSystem : EntitySystem
+{
+    [Dependency] private readonly AlertsSystem _alerts = default!;
+    [Dependency] private readonly InventorySystem _inventory = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private readonly SharedGasTankSystem _gasTank = default!;
+    [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<InternalsComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs);
+
+        SubscribeLocalEvent<InternalsComponent, ComponentStartup>(OnInternalsStartup);
+        SubscribeLocalEvent<InternalsComponent, ComponentShutdown>(OnInternalsShutdown);
+
+        SubscribeLocalEvent<InternalsComponent, InternalsDoAfterEvent>(OnDoAfter);
+        SubscribeLocalEvent<InternalsComponent, ToggleInternalsAlertEvent>(OnToggleInternalsAlert);
+    }
+
+    private void OnGetInteractionVerbs(
+        Entity<InternalsComponent> ent,
+        ref GetVerbsEvent<InteractionVerb> args)
+    {
+        if (!args.CanAccess || !args.CanInteract || args.Hands is null)
+            return;
+
+        if (!AreInternalsWorking(ent) && ent.Comp.BreathTools.Count == 0)
+            return;
+
+        var user = args.User;
+
+        InteractionVerb verb = new()
+        {
+            Act = () =>
+            {
+                ToggleInternals(ent, user, force: false, ent);
+            },
+            Message = Loc.GetString("action-description-internals-toggle"),
+            Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/dot.svg.192dpi.png")),
+            Text = Loc.GetString("action-name-internals-toggle"),
+        };
+
+        args.Verbs.Add(verb);
+    }
+
+    public bool ToggleInternals(
+        EntityUid uid,
+        EntityUid user,
+        bool force,
+        InternalsComponent? internals = null)
+    {
+        if (!Resolve(uid, ref internals, logMissing: false))
+            return false;
+
+        // Check if a mask is present.
+        if (internals.BreathTools.Count == 0)
+        {
+            _popupSystem.PopupClient(Loc.GetString("internals-no-breath-tool"), uid, user);
+            return false;
+        }
+
+        // Start the toggle do-after if it's on someone else.
+        if (!force && user != uid)
+        {
+            return StartToggleInternalsDoAfter(user, (uid, internals));
+        }
+
+        // Toggle off.
+        if (TryComp(internals.GasTankEntity, out GasTankComponent? gas))
+        {
+            return _gasTank.DisconnectFromInternals((internals.GasTankEntity.Value, gas), user);
+        }
+        else
+        {
+            // Check if tank is present.
+            var tank = FindBestGasTank(uid);
+
+            // If they're not on then check if we have a mask to use
+            if (tank == null)
+            {
+                _popupSystem.PopupClient(Loc.GetString("internals-no-tank"), uid, user);
+                return false;
+            }
+
+            return _gasTank.ConnectToInternals(tank.Value, user: user);
+        }
+    }
+
+    private bool StartToggleInternalsDoAfter(EntityUid user, Entity<InternalsComponent> targetEnt)
+    {
+        // Is the target not you? If yes, use a do-after to give them time to respond.
+        var isUser = user == targetEnt.Owner;
+        var delay = !isUser ? targetEnt.Comp.Delay : TimeSpan.Zero;
+
+        return _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, delay, new InternalsDoAfterEvent(), targetEnt, target: targetEnt)
+        {
+            BreakOnDamage = true,
+            BreakOnMove =  true,
+            MovementThreshold = 0.1f,
+        });
+    }
+
+    private void OnDoAfter(Entity<InternalsComponent> ent, ref InternalsDoAfterEvent args)
+    {
+        if (args.Cancelled || args.Handled)
+            return;
+
+        ToggleInternals(ent, args.User, force: true, ent);
+
+        args.Handled = true;
+    }
+
+    private void OnToggleInternalsAlert(Entity<InternalsComponent> ent, ref ToggleInternalsAlertEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        args.Handled |= ToggleInternals(ent, ent, false, internals: ent.Comp);
+    }
+
+    private void OnInternalsStartup(Entity<InternalsComponent> ent, ref ComponentStartup args)
+    {
+        _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
+    }
+
+    private void OnInternalsShutdown(Entity<InternalsComponent> ent, ref ComponentShutdown args)
+    {
+        _alerts.ClearAlert(ent, ent.Comp.InternalsAlert);
+    }
+
+    public void ConnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
+    {
+        if (!ent.Comp.BreathTools.Add(toolEntity))
+            return;
+
+        if (TryComp(toolEntity, out BreathToolComponent? breathTool))
+        {
+            breathTool.ConnectedInternalsEntity = ent.Owner;
+            Dirty(toolEntity, breathTool);
+        }
+
+        Dirty(ent);
+        _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
+    }
+
+    public void DisconnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity, bool forced = false)
+    {
+        if (!ent.Comp.BreathTools.Remove(toolEntity))
+            return;
+
+        Dirty(ent);
+
+        if (TryComp(toolEntity, out BreathToolComponent? breathTool))
+        {
+            breathTool.ConnectedInternalsEntity = null;
+            Dirty(toolEntity, breathTool);
+        }
+
+        if (ent.Comp.BreathTools.Count == 0)
+        {
+            DisconnectTank(ent, forced: forced);
+        }
+
+        _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
+    }
+
+    public void DisconnectTank(Entity<InternalsComponent> ent, bool forced = false)
+    {
+        if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
+            _gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank), forced: forced);
+
+        ent.Comp.GasTankEntity = null;
+        Dirty(ent);
+        _alerts.ShowAlert(ent.Owner, ent.Comp.InternalsAlert, GetSeverity(ent.Comp));
+    }
+
+    public bool TryConnectTank(Entity<InternalsComponent> ent, EntityUid tankEntity)
+    {
+        if (ent.Comp.BreathTools.Count == 0)
+            return false;
+
+        if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
+            _gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank));
+
+        ent.Comp.GasTankEntity = tankEntity;
+        Dirty(ent);
+        _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
+        return true;
+    }
+
+    public bool AreInternalsWorking(EntityUid uid, InternalsComponent? component = null)
+    {
+        return Resolve(uid, ref component, logMissing: false)
+               && AreInternalsWorking(component);
+    }
+
+    public bool AreInternalsWorking(InternalsComponent component)
+    {
+        return TryComp(component.BreathTools.FirstOrNull(), out BreathToolComponent? breathTool)
+               && breathTool.IsFunctional
+               && HasComp<GasTankComponent>(component.GasTankEntity);
+    }
+
+    protected short GetSeverity(InternalsComponent component)
+    {
+        if (component.BreathTools.Count == 0 || !AreInternalsWorking(component))
+            return 2;
+
+        // If pressure in the tank is below low pressure threshold, flash warning on internals UI
+        if (TryComp<GasTankComponent>(component.GasTankEntity, out var gasTank)
+            && gasTank.IsLowPressure)
+        {
+            return 0;
+        }
+
+        return 1;
+    }
+
+    public Entity<GasTankComponent>? FindBestGasTank(
+        Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user)
+    {
+        // TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses
+        // Prioritise
+        // 1. back equipped tanks
+        // 2. exo-slot tanks
+        // 3. in-hand tanks
+        // 4. pocket/belt tanks
+
+        if (!Resolve(user, ref user.Comp2, ref user.Comp3))
+            return null;
+
+        if (_inventory.TryGetSlotEntity(user, "back", out var backEntity, user.Comp2, user.Comp3) &&
+            TryComp<GasTankComponent>(backEntity, out var backGasTank) &&
+            _gasTank.CanConnectToInternals((backEntity.Value, backGasTank)))
+        {
+            return (backEntity.Value, backGasTank);
+        }
+
+        if (_inventory.TryGetSlotEntity(user, "suitstorage", out var entity, user.Comp2, user.Comp3) &&
+            TryComp<GasTankComponent>(entity, out var gasTank) &&
+            _gasTank.CanConnectToInternals((entity.Value, gasTank)))
+        {
+            return (entity.Value, gasTank);
+        }
+
+        foreach (var item in _inventory.GetHandOrInventoryEntities((user.Owner, user.Comp1, user.Comp2)))
+        {
+            if (TryComp(item, out gasTank) && _gasTank.CanConnectToInternals((item, gasTank)))
+                return (item, gasTank);
+        }
+
+        return null;
+    }
+}
index 3e899f3dc33b6c5b88ea7cb72092aaa98d6f6b62..4f89b111bda14243901ec2a742fc29c748505f82 100644 (file)
@@ -29,7 +29,10 @@ public sealed class MaskSystem : EntitySystem
     private void OnGetActions(EntityUid uid, MaskComponent component, GetItemActionsEvent args)
     {
         if (_inventorySystem.InSlotWithFlags(uid, SlotFlags.MASK))
+        {
             args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
+            Dirty(uid, component);
+        }
     }
 
     private void OnToggleMask(Entity<MaskComponent> ent, ref ToggleMaskEvent args)
@@ -59,8 +62,27 @@ public sealed class MaskSystem : EntitySystem
 
     private void OnGotUnequipped(EntityUid uid, MaskComponent mask, GotUnequippedEvent args)
     {
-        // Masks are currently always un-toggled when unequipped.
-        SetToggled((uid, mask), false);
+        if (!mask.IsToggled || !mask.IsToggleable)
+            return;
+
+        mask.IsToggled = false;
+        ToggleMaskComponents(uid, mask, args.Equipee, mask.EquippedPrefix, true);
+    }
+
+    /// <summary>
+    /// Called after setting IsToggled, raises events and dirties.
+    /// </summary>
+    private void ToggleMaskComponents(EntityUid uid, MaskComponent mask, EntityUid wearer, string? equippedPrefix = null, bool isEquip = false)
+    {
+        Dirty(uid, mask);
+        if (mask.ToggleActionEntity is {} action)
+            _actionSystem.SetToggled(action, mask.IsToggled);
+
+        var maskEv = new ItemMaskToggledEvent((wearer, mask), wearer);
+        RaiseLocalEvent(uid, ref maskEv);
+
+        var wearerEv = new WearerMaskToggledEvent((wearer, mask));
+        RaiseLocalEvent(wearer, ref wearerEv);
     }
 
     private void OnFolded(Entity<MaskComponent> ent, ref FoldedEvent args)
index 7d701ffd8741342ce32d702ec9cf4203102f849d..a2a9d8c556f171661fd89873817ce4e2522f70c7 100644 (file)
@@ -1,3 +1,4 @@
+using Robust.Shared.Audio;
 using Robust.Shared.GameStates;
 
 namespace Content.Shared.Lock;
@@ -14,4 +15,10 @@ public sealed partial class ActivatableUIRequiresLockComponent : Component
     /// </summary>
     [DataField]
     public bool RequireLocked;
+
+    /// <summary>
+    /// Sound to be played if an attempt is blocked.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier? AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
 }
index cbeceaf9e8be26240a76cdd85c805897e14a9e3e..444b3e28e615865bf23902e60f646bb333728034 100644 (file)
@@ -40,7 +40,7 @@ public sealed class LockSystem : EntitySystem
         base.Initialize();
 
         SubscribeLocalEvent<LockComponent, ComponentStartup>(OnStartup);
-        SubscribeLocalEvent<LockComponent, ActivateInWorldEvent>(OnActivated);
+        SubscribeLocalEvent<LockComponent, ActivateInWorldEvent>(OnActivated, before: [typeof(ActivatableUISystem)]);
         SubscribeLocalEvent<LockComponent, StorageOpenAttemptEvent>(OnStorageOpenAttempt);
         SubscribeLocalEvent<LockComponent, ExaminedEvent>(OnExamined);
         SubscribeLocalEvent<LockComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleLockVerb);
@@ -70,13 +70,11 @@ public sealed class LockSystem : EntitySystem
         // Only attempt an unlock by default on Activate
         if (lockComp.Locked && lockComp.UnlockOnClick)
         {
-            TryUnlock(uid, args.User, lockComp);
-            args.Handled = true;
+            args.Handled = TryUnlock(uid, args.User, lockComp);
         }
         else if (!lockComp.Locked && lockComp.LockOnClick)
         {
-            TryLock(uid, args.User, lockComp);
-            args.Handled = true;
+            args.Handled = TryLock(uid, args.User, lockComp);
         }
     }
 
@@ -400,7 +398,11 @@ public sealed class LockSystem : EntitySystem
         {
             args.Cancel();
             if (lockComp.Locked)
+            {
                 _sharedPopupSystem.PopupClient(Loc.GetString("entity-storage-component-locked-message"), uid, args.User);
+            }
+
+            _audio.PlayPredicted(component.AccessDeniedSound, uid, args.User);
         }
     }
 
diff --git a/Content.Shared/NodeContainer/Node.cs b/Content.Shared/NodeContainer/Node.cs
new file mode 100644 (file)
index 0000000..a4ab510
--- /dev/null
@@ -0,0 +1,100 @@
+using Content.Shared.NodeContainer.NodeGroups;
+using Robust.Shared.Map.Components;
+
+namespace Content.Shared.NodeContainer;
+
+/// <summary>
+///     Organizes themselves into distinct <see cref="INodeGroup"/>s with other <see cref="Node"/>s
+///     that they can "reach" and have the same <see cref="Node.NodeGroupID"/>.
+/// </summary>
+[ImplicitDataDefinitionForInheritors]
+public abstract partial class Node
+{
+    /// <summary>
+    ///     An ID used as a criteria for combining into groups. Determines which <see cref="INodeGroup"/>
+    ///     implementation is used as a group, detailed in <see cref="INodeGroupFactory"/>.
+    /// </summary>
+    [DataField("nodeGroupID")]
+    public NodeGroupID NodeGroupID { get; private set; } = NodeGroupID.Default;
+
+    /// <summary>
+    ///     The node group this node is a part of.
+    /// </summary>
+    [ViewVariables] public INodeGroup? NodeGroup;
+
+    /// <summary>
+    ///     The entity that owns this node via its <see cref="NodeContainerComponent"/>.
+    /// </summary>
+    [ViewVariables] public EntityUid Owner { get; private set; } = default!;
+
+    /// <summary>
+    ///     If this node should be considered for connection by other nodes.
+    /// </summary>
+    public virtual bool Connectable(IEntityManager entMan, TransformComponent? xform = null)
+    {
+        if (Deleting)
+            return false;
+
+        if (entMan.IsQueuedForDeletion(Owner))
+            return false;
+
+        if (!NeedAnchored)
+            return true;
+
+        xform ??= entMan.GetComponent<TransformComponent>(Owner);
+        return xform.Anchored;
+    }
+
+    [DataField]
+    public bool NeedAnchored { get; private set; } = true;
+
+    public virtual void OnAnchorStateChanged(IEntityManager entityManager, bool anchored) { }
+
+    /// <summary>
+    ///    Prevents a node from being used by other nodes while midway through removal.
+    /// </summary>
+    public bool Deleting;
+
+    /// <summary>
+    ///     All compatible nodes that are reachable by this node.
+    ///     Effectively, active connections out of this node.
+    /// </summary>
+    public readonly HashSet<Node> ReachableNodes = new();
+
+    public int FloodGen;
+    public int UndirectGen;
+    public bool FlaggedForFlood;
+    public int NetId;
+
+    /// <summary>
+    ///     Name of this node on the owning <see cref="NodeContainerComponent"/>.
+    /// </summary>
+    public string Name = default!;
+
+    /// <summary>
+    ///     Invoked when the owning <see cref="NodeContainerComponent"/> is initialized.
+    /// </summary>
+    /// <param name="owner">The owning entity.</param>
+    public virtual void Initialize(EntityUid owner, IEntityManager entMan)
+    {
+        Owner = owner;
+    }
+
+    /// <summary>
+    ///     How this node will attempt to find other reachable <see cref="Node"/>s to group with.
+    ///     Returns a set of <see cref="Node"/>s to consider grouping with. Should not return this current <see cref="Node"/>.
+    /// </summary>
+    /// <remarks>
+    /// <para>
+    /// The set of nodes returned can be asymmetrical
+    /// (meaning that it can return other nodes whose <see cref="GetReachableNodes"/> does not return this node).
+    /// If this is used, creation of a new node may not correctly merge networks unless both sides
+    /// of this asymmetric relation are made to manually update with <see cref="NodeGroupSystem.QueueReflood"/>.
+    /// </para>
+    /// </remarks>
+    public abstract IEnumerable<Node> GetReachableNodes(TransformComponent xform,
+        EntityQuery<NodeContainerComponent> nodeQuery,
+        EntityQuery<TransformComponent> xformQuery,
+        MapGridComponent? grid,
+        IEntityManager entMan);
+}
diff --git a/Content.Shared/NodeContainer/NodeContainerComponent.cs b/Content.Shared/NodeContainer/NodeContainerComponent.cs
new file mode 100644 (file)
index 0000000..674537b
--- /dev/null
@@ -0,0 +1,16 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.NodeContainer;
+
+/// <summary>
+///     Creates and maintains a set of <see cref="Rope.Node"/>s.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class NodeContainerComponent : Component
+{
+    //HACK: THIS BEING readOnly IS A FILTHY HACK AND I HATE IT --moony
+    [DataField(readOnly: true, serverOnly: true)] public Dictionary<string, Node> Nodes { get; private set; } = new();
+
+    [DataField] public bool Examinable = false;
+}
diff --git a/Content.Shared/NodeContainer/NodeGroups/INodeGroup.cs b/Content.Shared/NodeContainer/NodeGroups/INodeGroup.cs
new file mode 100644 (file)
index 0000000..da52d39
--- /dev/null
@@ -0,0 +1,33 @@
+using System.Linq;
+
+namespace Content.Shared.NodeContainer.NodeGroups;
+
+/// <summary>
+///     Maintains a collection of <see cref="Node"/>s, and performs operations requiring a list of
+///     all connected <see cref="Node"/>s.
+/// </summary>
+public interface INodeGroup
+{
+    bool Remaking { get; }
+
+    /// <summary>
+    ///     The list of nodes currently in this group.
+    /// </summary>
+    IReadOnlyList<Node> Nodes { get; }
+
+    void Create(NodeGroupID groupId);
+
+    void Initialize(Node sourceNode, IEntityManager entMan);
+
+    void RemoveNode(Node node);
+
+    void LoadNodes(List<Node> groupNodes);
+
+    // In theory, the SS13 curse ensures this method will never be called.
+    void AfterRemake(IEnumerable<IGrouping<INodeGroup?, Node>> newGroups);
+
+    /// <summary>
+    ///     Return any additional data to display for the node-visualizer debug overlay.
+    /// </summary>
+    string? GetDebugData();
+}
\ No newline at end of file
diff --git a/Content.Shared/NodeContainer/NodeGroups/NodeGroupID.cs b/Content.Shared/NodeContainer/NodeGroups/NodeGroupID.cs
new file mode 100644 (file)
index 0000000..214e9c5
--- /dev/null
@@ -0,0 +1,19 @@
+namespace Content.Shared.NodeContainer.NodeGroups;
+
+public enum NodeGroupID : byte
+{
+    Default,
+    HVPower,
+    MVPower,
+    Apc,
+    AMEngine,
+    Pipe,
+    WireNet,
+
+    /// <summary>
+    /// Group used by the TEG.
+    /// </summary>
+    /// <seealso cref="Content.Server.Power.Generation.Teg.TegSystem"/>
+    /// <seealso cref="Content.Server.Power.Generation.Teg.TegNodeGroup"/>
+    Teg,
+}
index c6a1818443a2e8637912346b2080ad4079d0e6ea..d02752e16b38e28f443e3b3693389bc020fcfd59 100644 (file)
@@ -90,7 +90,7 @@ public sealed class UseDelaySystem : EntitySystem
     /// </summary>
     public bool IsDelayed(Entity<UseDelayComponent?> ent, string id = DefaultId)
     {
-        if (!Resolve(ent, ref ent.Comp, false))
+        if (!Resolve(ent.Owner, ref ent.Comp, false))
             return false;
 
         if (!ent.Comp.Delays.TryGetValue(id, out var entry))
@@ -118,8 +118,14 @@ public sealed class UseDelaySystem : EntitySystem
     /// <param name="info"></param>
     /// <param name="id"></param>
     /// <returns></returns>
-    public bool TryGetDelayInfo(Entity<UseDelayComponent> ent, [NotNullWhen(true)] out UseDelayInfo? info, string id = DefaultId)
+    public bool TryGetDelayInfo(Entity<UseDelayComponent?> ent, [NotNullWhen(true)] out UseDelayInfo? info, string id = DefaultId)
     {
+        if (!Resolve(ent.Owner, ref ent.Comp, false))
+        {
+            info = null;
+            return false;
+        }
+
         return ent.Comp.Delays.TryGetValue(id, out info);
     }
 
index 1d453334e35c7d9416bacdaa65aae856fdf2562f..6aa505963300c2ef32528964d1c4b713cac177c5 100644 (file)
@@ -34,7 +34,11 @@ public sealed class ActivatableUIRequiresAnchorSystem : EntitySystem
 
         if (!Transform(ent.Owner).Anchored)
         {
-            _popup.PopupClient(Loc.GetString("comp-gas-pump-ui-needs-anchor"), args.User);
+            if (ent.Comp.Popup != null)
+            {
+                _popup.PopupClient(Loc.GetString(ent.Comp.Popup), args.User);
+            }
+
             args.Cancel();
         }
     }
index 7baf1f0b5d768418f73efae0dc20b2a4ee298ed0..0dd7db24321c3d079860a4043583783bb08af660 100644 (file)
     slots:
     - Back
     - suitStorage
+  - type: UseDelay
+    delays:
+      gasTank:
+        length: 1.0
   - type: ActivatableUI
     key: enum.SharedGasTankUiKey.Key
   - type: UserInterface
index a9dde098f934a3f3693d527f2161a0aae9726450..e304d21b87e960e45f1f6085051a4377bf0ac2b5 100644 (file)
       interfaces:
         enum.SharedGasTankUiKey.Key:
           type: GasTankBoundUserInterface
+    - type: UseDelay
+      delays:
+        gasTank:
+          length: 1.0
     - type: Clothing
       sprite: Objects/Tanks/Jetpacks/blue.rsi
       quickEquip: false
index 33c4dfbe178ebe18ab1fa0ecf282290c526cd125..f454c84480d0f535c4d32d0d26f39389321a857b 100644 (file)
@@ -35,6 +35,9 @@
             1: { state: can-o1, shader: "unshaded" }
             2: { state: can-o2, shader: "unshaded" }
             3: { state: can-o3, shader: "unshaded" }
+    - type: ActivatableUI
+      key: enum.GasCanisterUiKey.Key
+    - type: ActivatableUIRequiresLock
     - type: UserInterface
       interfaces:
         enum.GasCanisterUiKey.Key: