From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Fri, 2 May 2025 08:22:29 +0000 (+1000) Subject: Predicted internals (#33800) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=bd69fc612aee0cc0dd686672dd3ec23712684ce0;p=space-station-14.git Predicted internals (#33800) * 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 --- diff --git a/Content.Client/Alerts/ClientAlertsSystem.cs b/Content.Client/Alerts/ClientAlertsSystem.cs index 5b658fb07c..43c74adc5d 100644 --- a/Content.Client/Alerts/ClientAlertsSystem.cs +++ b/Content.Client/Alerts/ClientAlertsSystem.cs @@ -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>? SyncAlerts; @@ -27,6 +29,12 @@ public sealed class ClientAlertsSystem : AlertsSystem SubscribeLocalEvent(OnPlayerDetached); SubscribeLocalEvent(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 index 0000000000..696e7939f6 --- /dev/null +++ b/Content.Client/Atmos/EntitySystems/GasTankSystem.cs @@ -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(OnGasTankState); + } + + private void OnGasTankState(Entity ent, ref AfterAutoHandleStateEvent args) + { + if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui)) + { + bui.Update(); + } + } + + public override void UpdateUserInterface(Entity ent) + { + if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui)) + { + bui.Update(); + } + } +} diff --git a/Content.Client/Atmos/Piping/Unary/Systems/GasCanisterSystem.cs b/Content.Client/Atmos/Piping/Unary/Systems/GasCanisterSystem.cs new file mode 100644 index 0000000000..cae184edbb --- /dev/null +++ b/Content.Client/Atmos/Piping/Unary/Systems/GasCanisterSystem.cs @@ -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(OnGasState); + } + + private void OnGasState(Entity ent, ref AfterAutoHandleStateEvent args) + { + if (UI.TryGetOpenUi(ent.Owner, GasCanisterUiKey.Key, out var bui)) + { + bui.Update(); + } + } + + protected override void DirtyUI(EntityUid uid, GasCanisterComponent? component = null, NodeContainerComponent? nodes = null) + { + if (UI.TryGetOpenUi(uid, GasCanisterUiKey.Key, out var bui)) + { + bui.Update(); + } + } +} diff --git a/Content.Client/Atmos/UI/GasCanisterBoundUserInterface.cs b/Content.Client/Atmos/UI/GasCanisterBoundUserInterface.cs index 7bf9b396d5..0456426b1f 100644 --- a/Content.Client/Atmos/UI/GasCanisterBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasCanisterBoundUserInterface.cs @@ -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)); } /// @@ -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) diff --git a/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs b/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs index 3c3d8f1509..b959bc966b 100644 --- a/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs @@ -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 index 0000000000..87daac3722 --- /dev/null +++ b/Content.Client/Body/Systems/InternalsSystem.cs @@ -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(OnInternalsAfterState); + } + + private void OnInternalsAfterState(Entity ent, ref AfterAutoHandleStateEvent args) + { + if (ent.Comp.GasTankEntity != null && _ui.TryGetOpenUi(ent.Comp.GasTankEntity.Value, SharedGasTankUiKey.Key, out var bui)) + { + bui.Update(); + } + } +} diff --git a/Content.Client/UserInterface/Systems/Alerts/Controls/AlertControl.cs b/Content.Client/UserInterface/Systems/Alerts/Controls/AlertControl.cs index b0e2e39483..4e803a20b1 100644 --- a/Content.Client/UserInterface/Systems/Alerts/Controls/AlertControl.cs +++ b/Content.Client/UserInterface/Systems/Alerts/Controls/AlertControl.cs @@ -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; } /// @@ -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 /// severity of alert, null if alert doesn't have severity levels public AlertControl(AlertPrototype alert, short? severity) { - _gameTiming = IoCManager.Resolve(); - _entityManager = IoCManager.Resolve(); + // Alerts will handle this. + MuteSounds = true; + + IoCManager.InjectDependencies(this); TooltipSupplier = SupplyTooltip; Alert = alert; _severity = severity; diff --git a/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankBoundUserInterface.cs b/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankBoundUserInterface.cs index 4ae74a5d65..19e578fda3 100644 --- a/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankBoundUserInterface.cs +++ b/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankBoundUserInterface.cs @@ -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(); + _window.Entity = Owner; _window.SetTitle(EntMan.GetComponent(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().CanConnectToInternals((Owner, component)); + _window?.Update(canConnect, component.IsConnected, component.OutputPressure); + } + if (state is GasTankBoundUserInterfaceState cast) _window?.UpdateState(cast); } diff --git a/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankWindow.cs b/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankWindow.cs index fd5624ad8a..638df36504 100644 --- a/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankWindow.cs +++ b/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankWindow.cs @@ -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? 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().CanConnectToInternals((Entity, tank)); + _btnInternals.Disabled = !canConnectInternals; + } + + if (!_btnInternals.Disabled) { - _spbPressure.Value = state.OutputPressure.Value; + _btnInternals.Disabled = _entManager.System().IsDelayed(Entity, id: SharedGasTankSystem.GasTankDelay); } } diff --git a/Content.IntegrationTests/Tests/Power/PowerTest.cs b/Content.IntegrationTests/Tests/Power/PowerTest.cs index 55bb42f8ce..a448427d05 100644 --- a/Content.IntegrationTests/Tests/Power/PowerTest.cs +++ b/Content.IntegrationTests/Tests/Power/PowerTest.cs @@ -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; diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs index 8b0a24d902..ccc7a98a1e 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs @@ -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; diff --git a/Content.Server/Ame/AmeNodeGroup.cs b/Content.Server/Ame/AmeNodeGroup.cs index f4eb1bb302..f9cec6c5e7 100644 --- a/Content.Server/Ame/AmeNodeGroup.cs +++ b/Content.Server/Ame/AmeNodeGroup.cs @@ -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; diff --git a/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs b/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs index 7e84cbc743..ff6cdd2da7 100644 --- a/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs +++ b/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs @@ -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 index ae17a5d872..0000000000 --- a/Content.Server/Atmos/Components/BreathToolComponent.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Content.Shared.Inventory; - -namespace Content.Server.Atmos.Components -{ - /// - /// Used in internals as breath tool. - /// - [RegisterComponent] - [ComponentProtoName("BreathMask")] - public sealed partial class BreathToolComponent : Component - { - /// - /// Tool is functional only in allowed slots - /// - [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 index 2d6b073e9d..0000000000 --- a/Content.Server/Atmos/Components/GasTankComponent.cs +++ /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(); - - /// - /// Pressure at which tank should be considered 'low' such as for internals. - /// - [DataField("tankLowPressure"), ViewVariables(VVAccess.ReadWrite)] - public float TankLowPressure = DefaultLowPressure; - - /// - /// Distributed pressure. - /// - [DataField("outputPressure"), ViewVariables(VVAccess.ReadWrite)] - public float OutputPressure = DefaultOutputPressure; - - /// - /// The maximum allowed output pressure. - /// - [DataField("maxOutputPressure"), ViewVariables(VVAccess.ReadWrite)] - public float MaxOutputPressure = 3 * DefaultOutputPressure; - - /// - /// Tank is connected to internals. - /// - [ViewVariables] - public bool IsConnected => User != null; - - [ViewVariables] - public EntityUid? User; - - /// - /// 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. - /// - [ViewVariables] - public bool CheckUser; - - /// - /// Pressure at which tanks start leaking. - /// - [DataField("tankLeakPressure"), ViewVariables(VVAccess.ReadWrite)] - public float TankLeakPressure = 30 * Atmospherics.OneAtmosphere; - - /// - /// Pressure at which tank spills all contents into atmosphere. - /// - [DataField("tankRupturePressure"), ViewVariables(VVAccess.ReadWrite)] - public float TankRupturePressure = 40 * Atmospherics.OneAtmosphere; - - /// - /// Base 3x3 explosion. - /// - [DataField("tankFragmentPressure"), ViewVariables(VVAccess.ReadWrite)] - public float TankFragmentPressure = 50 * Atmospherics.OneAtmosphere; - - /// - /// Increases explosion for each scale kPa above threshold. - /// - [DataField("tankFragmentScale"), ViewVariables(VVAccess.ReadWrite)] - public float TankFragmentScale = 2 * Atmospherics.OneAtmosphere; - - [DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string ToggleAction = "ActionToggleInternals"; - - [DataField("toggleActionEntity")] public EntityUid? ToggleActionEntity; - - /// - /// Valve to release gas from tank - /// - [DataField("isValveOpen"), ViewVariables(VVAccess.ReadWrite)] - public bool IsValveOpen = false; - - /// - /// Gas release rate in L/s - /// - [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), - }; - } -} diff --git a/Content.Server/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs b/Content.Server/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs index 8c786e5b21..532ba04d29 100644 --- a/Content.Server/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs +++ b/Content.Server/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs @@ -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 index 327804f39a..0000000000 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BreathTool.cs +++ /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(OnBreathToolShutdown); - } - - private void OnBreathToolShutdown(Entity entity, ref ComponentShutdown args) - { - DisconnectInternals(entity); - } - - public void DisconnectInternals(Entity entity) - { - var old = entity.Comp.ConnectedInternalsEntity; - entity.Comp.ConnectedInternalsEntity = null; - - if (TryComp(old, out var internalsComponent)) - { - _internals.DisconnectBreathTool((old.Value, internalsComponent), entity.Owner); - } - - entity.Comp.IsFunctional = false; - } -} diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.GridAtmosphere.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.GridAtmosphere.cs index b11eb5dc3e..72b6e9d745 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.GridAtmosphere.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.GridAtmosphere.cs @@ -44,8 +44,6 @@ public sealed partial class AtmosphereSystem private void OnGridAtmosphereInit(EntityUid uid, GridAtmosphereComponent component, ComponentInit args) { - base.Initialize(); - EnsureComp(uid); foreach (var tile in component.Tiles.Values) { diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs index f27f7411bf..0f5bc1af04 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs @@ -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(); diff --git a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs index cc541e35e9..c3f934549b 100644 --- a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs @@ -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; diff --git a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs index 4d409a7082..08177a7d95 100644 --- a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs @@ -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(OnGasShutdown); - SubscribeLocalEvent(BeforeUiOpen); - SubscribeLocalEvent(OnGetActions); - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnActionToggle); SubscribeLocalEvent(OnParentChange); - SubscribeLocalEvent(OnGasTankSetPressure); - SubscribeLocalEvent(OnGasTankToggleInternals); SubscribeLocalEvent(OnAnalyzed); SubscribeLocalEvent(OnGasTankPrice); - SubscribeLocalEvent>(OnGetAlternativeVerb); Subs.CVar(_cfg, CCVars.AtmosTankFragment, UpdateMaxRange, true); } @@ -63,44 +44,16 @@ namespace Content.Server.Atmos.EntitySystems _maxExplosionRange = value; } - private void OnGasShutdown(Entity gasTank, ref ComponentShutdown args) - { - DisconnectFromInternals(gasTank); - } - - private void OnGasTankToggleInternals(Entity ent, ref GasTankToggleInternalsMessage args) - { - ToggleInternals(ent); - } - - private void OnGasTankSetPressure(Entity 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 ent, bool initialUpdate = false) + public override void UpdateUserInterface(Entity 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 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 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 ent) - { - if (ent.Comp.IsConnected) - { - DisconnectFromInternals(ent); - } - else - { - ConnectToInternals(ent); - } - } - public GasMixture? RemoveAir(Entity 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 ent) - { - TryGetInternalsComp(ent, out _, out var internalsComp, ent.Comp.User); - return internalsComp != null && internalsComp.BreathTools.Count != 0 && !ent.Comp.IsValveOpen; - } - - public void ConnectToInternals(Entity 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 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); - } - - /// - /// Tries to retrieve the internals component of either the gas tank's user, - /// or the gas tank's... containing container - /// - /// The user of the gas tank - /// True if internals comp isn't null, false if it is null - private bool TryGetInternalsComp(Entity 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(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(container.Owner, out var containerInternalsComp) && containerInternalsComp != null) - { - internalsUid = container.Owner; - internalsComp = containerInternalsComp; - return true; - } - } - - return false; - } - public void AssumeAir(Entity 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 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, - }); - } } } diff --git a/Content.Server/Atmos/EntitySystems/PipeRestrictOverlapSystem.cs b/Content.Server/Atmos/EntitySystems/PipeRestrictOverlapSystem.cs index c2ff87ca79..f64aff47f4 100644 --- a/Content.Server/Atmos/EntitySystems/PipeRestrictOverlapSystem.cs +++ b/Content.Server/Atmos/EntitySystems/PipeRestrictOverlapSystem.cs @@ -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 index 65d7ba69a7..0000000000 --- a/Content.Server/Atmos/IGasMixtureHolder.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Content.Shared.Atmos; - -namespace Content.Server.Atmos -{ - public interface IGasMixtureHolder - { - public GasMixture Air { get; set; } - } -} diff --git a/Content.Server/Atmos/Piping/EntitySystems/AtmosPipeAppearanceSystem.cs b/Content.Server/Atmos/Piping/EntitySystems/AtmosPipeAppearanceSystem.cs index 10049e273b..53cc35463c 100644 --- a/Content.Server/Atmos/Piping/EntitySystems/AtmosPipeAppearanceSystem.cs +++ b/Content.Server/Atmos/Piping/EntitySystems/AtmosPipeAppearanceSystem.cs @@ -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; diff --git a/Content.Server/Atmos/Piping/EntitySystems/AtmosUnsafeUnanchorSystem.cs b/Content.Server/Atmos/Piping/EntitySystems/AtmosUnsafeUnanchorSystem.cs index bc925e433c..25439736c7 100644 --- a/Content.Server/Atmos/Piping/EntitySystems/AtmosUnsafeUnanchorSystem.cs +++ b/Content.Server/Atmos/Piping/EntitySystems/AtmosUnsafeUnanchorSystem.cs @@ -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 index afbfb91249..0000000000 --- a/Content.Server/Atmos/Piping/Unary/Components/GasCanisterComponent.cs +++ /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"; - - /// - /// Container name for the gas tank holder. - /// - [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(); - - /// - /// Last recorded pressure, for appearance-updating purposes. - /// - public float LastPressure { get; set; } = 0f; - - /// - /// Minimum release pressure possible for the release valve. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("minReleasePressure")] - public float MinReleasePressure { get; set; } = Atmospherics.OneAtmosphere / 10; - - /// - /// Maximum release pressure possible for the release valve. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("maxReleasePressure")] - public float MaxReleasePressure { get; set; } = Atmospherics.OneAtmosphere * 10; - - /// - /// Valve release pressure. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("releasePressure")] - public float ReleasePressure { get; set; } = Atmospherics.OneAtmosphere; - - /// - /// Whether the release valve is open on the canister. - /// - [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 - } -} diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs index 292b6d94f8..a8f505ca5d 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs @@ -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(OnCanisterStartup); SubscribeLocalEvent(OnCanisterUpdated); - SubscribeLocalEvent(OnCanisterActivate, after: new[] { typeof(LockSystem) }); - SubscribeLocalEvent(OnCanisterInteractHand); - SubscribeLocalEvent(OnCanisterInsertAttempt); - SubscribeLocalEvent(OnCanisterContainerInserted); - SubscribeLocalEvent(OnCanisterContainerRemoved); SubscribeLocalEvent(CalculateCanisterPrice); SubscribeLocalEvent(OnAnalyzed); - // Bound UI subscriptions - SubscribeLocalEvent(OnHoldingTankEjectMessage); - SubscribeLocalEvent(OnCanisterChangeReleasePressure); - SubscribeLocalEvent(OnCanisterChangeReleaseValve); } /// @@ -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(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 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(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(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(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); - } - /// /// 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)); } } - - /// - /// Check if the canister is locked, playing its sound and popup if so. - /// - /// - /// True if locked, false otherwise. - /// - private bool CheckLocked(EntityUid uid, GasCanisterComponent comp, EntityUid user) - { - if (TryComp(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 index a7edab4a6b..0000000000 --- a/Content.Server/Body/Components/InternalsComponent.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Content.Shared.Alert; -using Robust.Shared.Prototypes; - -namespace Content.Server.Body.Components -{ - /// - /// Handles hooking up a mask (breathing tool) / gas tank together and allowing the Owner to breathe through it. - /// - [RegisterComponent] - public sealed partial class InternalsComponent : Component - { - [ViewVariables] - public EntityUid? GasTankEntity; - - [ViewVariables] - public HashSet BreathTools { get; set; } = new(); - - /// - /// Toggle Internals delay when the target is not you. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField] - public TimeSpan Delay = TimeSpan.FromSeconds(3); - - [DataField] - public ProtoId InternalsAlert = "Internals"; - } - -} diff --git a/Content.Server/Body/Systems/InternalsSystem.cs b/Content.Server/Body/Systems/InternalsSystem.cs index 7e86cb6f07..29c2c363d2 100644 --- a/Content.Server/Body/Systems/InternalsSystem.cs +++ b/Content.Server/Body/Systems/InternalsSystem.cs @@ -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 _internalsQuery; @@ -34,12 +27,6 @@ public sealed class InternalsSystem : EntitySystem _internalsQuery = GetEntityQuery(); SubscribeLocalEvent(OnInhaleLocation); - SubscribeLocalEvent(OnInternalsStartup); - SubscribeLocalEvent(OnInternalsShutdown); - SubscribeLocalEvent>(OnGetInteractionVerbs); - SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(OnToggleInternalsAlert); - SubscribeLocalEvent(OnStartingGear); } @@ -66,120 +53,6 @@ public sealed class InternalsSystem : EntitySystem ToggleInternals(uid, uid, force: false, component); } - private void OnGetInteractionVerbs( - Entity ent, - ref GetVerbsEvent 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 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 ent, ref InternalsDoAfterEvent args) - { - if (args.Cancelled || args.Handled) - return; - - ToggleInternals(ent, args.User, force: true, ent); - - args.Handled = true; - } - - private void OnToggleInternalsAlert(Entity ent, ref ToggleInternalsAlertEvent args) - { - if (args.Handled) - return; - ToggleInternals(ent, ent, false, internals: ent.Comp); - args.Handled = true; - } - - private void OnInternalsStartup(Entity ent, ref ComponentStartup args) - { - _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent)); - } - - private void OnInternalsShutdown(Entity ent, ref ComponentShutdown args) - { - _alerts.ClearAlert(ent, ent.Comp.InternalsAlert); - } - private void OnInhaleLocation(Entity 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 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 ent, EntityUid toolEntity) - { - if (!ent.Comp.BreathTools.Add(toolEntity)) - return; - - _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent)); - } - - public void DisconnectTank(Entity 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 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(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(component.GasTankEntity, out var gasTank) - && gasTank.IsLowPressure) - { - return 0; - } - - return 1; - } - - public Entity? FindBestGasTank( - Entity 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(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(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; - } } diff --git a/Content.Server/Body/Systems/LungSystem.cs b/Content.Server/Body/Systems/LungSystem.cs index 82ec490a4a..273a8466ca 100644 --- a/Content.Server/Body/Systems/LungSystem.cs +++ b/Content.Server/Body/Systems/LungSystem.cs @@ -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(OnComponentInit); SubscribeLocalEvent(OnGotEquipped); SubscribeLocalEvent(OnGotUnequipped); - SubscribeLocalEvent(OnMaskToggled); } private void OnGotUnequipped(Entity 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 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)) diff --git a/Content.Server/DeviceLinking/Systems/PowerSensorSystem.cs b/Content.Server/DeviceLinking/Systems/PowerSensorSystem.cs index 9975f04bbb..b03cceda0f 100644 --- a/Content.Server/DeviceLinking/Systems/PowerSensorSystem.cs +++ b/Content.Server/DeviceLinking/Systems/PowerSensorSystem.cs @@ -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; diff --git a/Content.Server/DeviceNetwork/Components/ApcNetworkComponent.cs b/Content.Server/DeviceNetwork/Components/ApcNetworkComponent.cs index 4f021e7b71..ad13f9211d 100644 --- a/Content.Server/DeviceNetwork/Components/ApcNetworkComponent.cs +++ b/Content.Server/DeviceNetwork/Components/ApcNetworkComponent.cs @@ -1,5 +1,6 @@ using Content.Server.DeviceNetwork.Systems; using Content.Server.NodeContainer.Nodes; +using Content.Shared.NodeContainer; namespace Content.Server.DeviceNetwork.Components { diff --git a/Content.Server/DeviceNetwork/Systems/ApcNetworkSystem.cs b/Content.Server/DeviceNetwork/Systems/ApcNetworkSystem.cs index 798a9b540e..288732bb0a 100644 --- a/Content.Server/DeviceNetwork/Systems/ApcNetworkSystem.cs +++ b/Content.Server/DeviceNetwork/Systems/ApcNetworkSystem.cs @@ -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 { diff --git a/Content.Server/Electrocution/ElectrocutionNode.cs b/Content.Server/Electrocution/ElectrocutionNode.cs index c8e437d353..ddf09fcce7 100644 --- a/Content.Server/Electrocution/ElectrocutionNode.cs +++ b/Content.Server/Electrocution/ElectrocutionNode.cs @@ -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 diff --git a/Content.Server/Electrocution/ElectrocutionSystem.cs b/Content.Server/Electrocution/ElectrocutionSystem.cs index 0059a8b427..957f881bed 100644 --- a/Content.Server/Electrocution/ElectrocutionSystem.cs +++ b/Content.Server/Electrocution/ElectrocutionSystem.cs @@ -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; diff --git a/Content.Server/Movement/Systems/JetpackSystem.cs b/Content.Server/Movement/Systems/JetpackSystem.cs index 546336890f..2ca807ae72 100644 --- a/Content.Server/Movement/Systems/JetpackSystem.cs +++ b/Content.Server/Movement/Systems/JetpackSystem.cs @@ -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; diff --git a/Content.Server/NodeContainer/EntitySystems/NodeContainerSystem.cs b/Content.Server/NodeContainer/EntitySystems/NodeContainerSystem.cs index c0603949be..ac818e08dc 100644 --- a/Content.Server/NodeContainer/EntitySystems/NodeContainerSystem.cs +++ b/Content.Server/NodeContainer/EntitySystems/NodeContainerSystem.cs @@ -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 diff --git a/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs b/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs index 62806fe84f..7b55e20f8a 100644 --- a/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs +++ b/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs @@ -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 index de4586d9ac..0000000000 --- a/Content.Server/NodeContainer/NodeContainerComponent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Content.Server.NodeContainer.Nodes; - -namespace Content.Server.NodeContainer -{ - /// - /// Creates and maintains a set of s. - /// - [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 Nodes { get; private set; } = new(); - - [DataField("examinable")] public bool Examinable = false; - } -} diff --git a/Content.Server/NodeContainer/NodeGroups/BaseNodeGroup.cs b/Content.Server/NodeContainer/NodeGroups/BaseNodeGroup.cs index 1481d7b1c2..052d158c1a 100644 --- a/Content.Server/NodeContainer/NodeGroups/BaseNodeGroup.cs +++ b/Content.Server/NodeContainer/NodeGroups/BaseNodeGroup.cs @@ -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 { - /// - /// Maintains a collection of s, and performs operations requiring a list of - /// all connected s. - /// - public interface INodeGroup - { - bool Remaking { get; } - - /// - /// The list of nodes currently in this group. - /// - IReadOnlyList Nodes { get; } - - void Create(NodeGroupID groupId); - - void Initialize(Node sourceNode, IEntityManager entMan); - - void RemoveNode(Node node); - - void LoadNodes(List groupNodes); - - // In theory, the SS13 curse ensures this method will never be called. - void AfterRemake(IEnumerable> newGroups); - - /// - /// Return any additional data to display for the node-visualizer debug overlay. - /// - string? GetDebugData(); - } - [NodeGroup(NodeGroupID.Default, NodeGroupID.WireNet)] [Virtual] public class BaseNodeGroup : INodeGroup diff --git a/Content.Server/NodeContainer/NodeGroups/NodeGroupAttribute.cs b/Content.Server/NodeContainer/NodeGroups/NodeGroupAttribute.cs index 9b4979a524..9200be790f 100644 --- a/Content.Server/NodeContainer/NodeGroups/NodeGroupAttribute.cs +++ b/Content.Server/NodeContainer/NodeGroups/NodeGroupAttribute.cs @@ -1,3 +1,4 @@ +using Content.Shared.NodeContainer.NodeGroups; using JetBrains.Annotations; namespace Content.Server.NodeContainer.NodeGroups diff --git a/Content.Server/NodeContainer/NodeGroups/NodeGroupFactory.cs b/Content.Server/NodeContainer/NodeGroups/NodeGroupFactory.cs index abebfd1a90..71ca91f0f9 100644 --- a/Content.Server/NodeContainer/NodeGroups/NodeGroupFactory.cs +++ b/Content.Server/NodeContainer/NodeGroups/NodeGroupFactory.cs @@ -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, - - /// - /// Group used by the TEG. - /// - /// - /// - Teg, - } } diff --git a/Content.Server/NodeContainer/NodeGroups/PipeNet.cs b/Content.Server/NodeContainer/NodeGroups/PipeNet.cs index e905a6e78e..3c8e65ca91 100644 --- a/Content.Server/NodeContainer/NodeGroups/PipeNet.cs +++ b/Content.Server/NodeContainer/NodeGroups/PipeNet.cs @@ -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 diff --git a/Content.Server/NodeContainer/Nodes/AdjacentNode.cs b/Content.Server/NodeContainer/Nodes/AdjacentNode.cs index 6df534b285..d719ccbff0 100644 --- a/Content.Server/NodeContainer/Nodes/AdjacentNode.cs +++ b/Content.Server/NodeContainer/Nodes/AdjacentNode.cs @@ -1,3 +1,4 @@ +using Content.Shared.NodeContainer; using Robust.Shared.Map; using Robust.Shared.Map.Components; diff --git a/Content.Server/NodeContainer/Nodes/IRotatableNode.cs b/Content.Server/NodeContainer/Nodes/IRotatableNode.cs index 38b6dbdf19..b2b51367a1 100644 --- a/Content.Server/NodeContainer/Nodes/IRotatableNode.cs +++ b/Content.Server/NodeContainer/Nodes/IRotatableNode.cs @@ -1,3 +1,5 @@ +using Content.Shared.NodeContainer; + namespace Content.Server.NodeContainer.Nodes { /// diff --git a/Content.Server/NodeContainer/Nodes/Node.cs b/Content.Server/NodeContainer/Nodes/Node.cs deleted file mode 100644 index fe9299e453..0000000000 --- a/Content.Server/NodeContainer/Nodes/Node.cs +++ /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 -{ - /// - /// Organizes themselves into distinct s with other s - /// that they can "reach" and have the same . - /// - [ImplicitDataDefinitionForInheritors] - public abstract partial class Node - { - /// - /// An ID used as a criteria for combining into groups. Determines which - /// implementation is used as a group, detailed in . - /// - [DataField("nodeGroupID")] - public NodeGroupID NodeGroupID { get; private set; } = NodeGroupID.Default; - - /// - /// The node group this node is a part of. - /// - [ViewVariables] public INodeGroup? NodeGroup; - - /// - /// The entity that owns this node via its . - /// - [ViewVariables] public EntityUid Owner { get; private set; } = default!; - - /// - /// If this node should be considered for connection by other nodes. - /// - 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(Owner); - return xform.Anchored; - } - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("needAnchored")] - public bool NeedAnchored { get; private set; } = true; - - public virtual void OnAnchorStateChanged(IEntityManager entityManager, bool anchored) { } - - /// - /// Prevents a node from being used by other nodes while midway through removal. - /// - public bool Deleting; - - /// - /// All compatible nodes that are reachable by this node. - /// Effectively, active connections out of this node. - /// - public readonly HashSet ReachableNodes = new(); - - internal int FloodGen; - internal int UndirectGen; - internal bool FlaggedForFlood; - internal int NetId; - - /// - /// Name of this node on the owning . - /// - public string Name = default!; - - /// - /// Invoked when the owning is initialized. - /// - /// The owning entity. - public virtual void Initialize(EntityUid owner, IEntityManager entMan) - { - Owner = owner; - } - - /// - /// How this node will attempt to find other reachable s to group with. - /// Returns a set of s to consider grouping with. Should not return this current . - /// - /// - /// - /// The set of nodes returned can be asymmetrical - /// (meaning that it can return other nodes whose 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 . - /// - /// - public abstract IEnumerable GetReachableNodes(TransformComponent xform, - EntityQuery nodeQuery, - EntityQuery xformQuery, - MapGridComponent? grid, - IEntityManager entMan); - } -} diff --git a/Content.Server/NodeContainer/Nodes/NodeHelpers.cs b/Content.Server/NodeContainer/Nodes/NodeHelpers.cs index ad6d59dbaf..c2345fff76 100644 --- a/Content.Server/NodeContainer/Nodes/NodeHelpers.cs +++ b/Content.Server/NodeContainer/Nodes/NodeHelpers.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Content.Shared.NodeContainer; using Robust.Shared.Map; using Robust.Shared.Map.Components; diff --git a/Content.Server/NodeContainer/Nodes/PipeNode.cs b/Content.Server/NodeContainer/Nodes/PipeNode.cs index 31ee571249..d30d3b1777 100644 --- a/Content.Server/NodeContainer/Nodes/PipeNode.cs +++ b/Content.Server/NodeContainer/Nodes/PipeNode.cs @@ -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; diff --git a/Content.Server/NodeContainer/Nodes/PortPipeNode.cs b/Content.Server/NodeContainer/Nodes/PortPipeNode.cs index 69d85b4224..04e0dc0ab7 100644 --- a/Content.Server/NodeContainer/Nodes/PortPipeNode.cs +++ b/Content.Server/NodeContainer/Nodes/PortPipeNode.cs @@ -1,3 +1,4 @@ +using Content.Shared.NodeContainer; using Robust.Shared.Map; using Robust.Shared.Map.Components; diff --git a/Content.Server/NodeContainer/Nodes/PortablePipeNode.cs b/Content.Server/NodeContainer/Nodes/PortablePipeNode.cs index 287cd6b3b5..427288ee50 100644 --- a/Content.Server/NodeContainer/Nodes/PortablePipeNode.cs +++ b/Content.Server/NodeContainer/Nodes/PortablePipeNode.cs @@ -1,3 +1,4 @@ +using Content.Shared.NodeContainer; using Robust.Shared.Map; using Robust.Shared.Map.Components; diff --git a/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs b/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs index 7882522d30..3cf25472f0 100644 --- a/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs +++ b/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs @@ -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; diff --git a/Content.Server/Power/Components/BaseNetConnectorComponent.cs b/Content.Server/Power/Components/BaseNetConnectorComponent.cs index f2a1372dbc..b22950c1a1 100644 --- a/Content.Server/Power/Components/BaseNetConnectorComponent.cs +++ b/Content.Server/Power/Components/BaseNetConnectorComponent.cs @@ -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 { diff --git a/Content.Server/Power/Components/PowerMonitoringDeviceComponent.cs b/Content.Server/Power/Components/PowerMonitoringDeviceComponent.cs index c8f90d9ad5..f01710fbe1 100644 --- a/Content.Server/Power/Components/PowerMonitoringDeviceComponent.cs +++ b/Content.Server/Power/Components/PowerMonitoringDeviceComponent.cs @@ -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; diff --git a/Content.Server/Power/EntitySystems/CableMultitoolSystem.cs b/Content.Server/Power/EntitySystems/CableMultitoolSystem.cs index 761f274ba5..485f0c00b4 100644 --- a/Content.Server/Power/EntitySystems/CableMultitoolSystem.cs +++ b/Content.Server/Power/EntitySystems/CableMultitoolSystem.cs @@ -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; diff --git a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs index 0fc641ed28..7b41af66b9 100644 --- a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs @@ -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; diff --git a/Content.Server/Power/Generation/Teg/TegNodeGroup.cs b/Content.Server/Power/Generation/Teg/TegNodeGroup.cs index 3c937f8f71..92a353ccb1 100644 --- a/Content.Server/Power/Generation/Teg/TegNodeGroup.cs +++ b/Content.Server/Power/Generation/Teg/TegNodeGroup.cs @@ -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; diff --git a/Content.Server/Power/Generation/Teg/TegSystem.cs b/Content.Server/Power/Generation/Teg/TegSystem.cs index 04f876c2c2..77848d5c79 100644 --- a/Content.Server/Power/Generation/Teg/TegSystem.cs +++ b/Content.Server/Power/Generation/Teg/TegSystem.cs @@ -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; diff --git a/Content.Server/Power/Generator/PowerSwitchableSystem.cs b/Content.Server/Power/Generator/PowerSwitchableSystem.cs index 61892f23f8..7b7d7ce256 100644 --- a/Content.Server/Power/Generator/PowerSwitchableSystem.cs +++ b/Content.Server/Power/Generator/PowerSwitchableSystem.cs @@ -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; diff --git a/Content.Server/Power/NodeGroups/ApcNet.cs b/Content.Server/Power/NodeGroups/ApcNet.cs index 8c0b89b507..60fad61fcc 100644 --- a/Content.Server/Power/NodeGroups/ApcNet.cs +++ b/Content.Server/Power/NodeGroups/ApcNet.cs @@ -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 diff --git a/Content.Server/Power/NodeGroups/BaseNetConnectorNodeGroup.cs b/Content.Server/Power/NodeGroups/BaseNetConnectorNodeGroup.cs index d70fbceed3..6f5673796a 100644 --- a/Content.Server/Power/NodeGroups/BaseNetConnectorNodeGroup.cs +++ b/Content.Server/Power/NodeGroups/BaseNetConnectorNodeGroup.cs @@ -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 { diff --git a/Content.Server/Power/NodeGroups/BasePowerNet.cs b/Content.Server/Power/NodeGroups/BasePowerNet.cs index 1628702225..15ed2630d4 100644 --- a/Content.Server/Power/NodeGroups/BasePowerNet.cs +++ b/Content.Server/Power/NodeGroups/BasePowerNet.cs @@ -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; diff --git a/Content.Server/Power/NodeGroups/PowerNet.cs b/Content.Server/Power/NodeGroups/PowerNet.cs index edbc5661e2..ab84623be4 100644 --- a/Content.Server/Power/NodeGroups/PowerNet.cs +++ b/Content.Server/Power/NodeGroups/PowerNet.cs @@ -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 { diff --git a/Content.Server/Power/Nodes/CableDeviceNode.cs b/Content.Server/Power/Nodes/CableDeviceNode.cs index d7914f2b2e..4089cd5657 100644 --- a/Content.Server/Power/Nodes/CableDeviceNode.cs +++ b/Content.Server/Power/Nodes/CableDeviceNode.cs @@ -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 diff --git a/Content.Server/Power/Nodes/CableNode.cs b/Content.Server/Power/Nodes/CableNode.cs index 4f2dbd42df..b8c404e8e2 100644 --- a/Content.Server/Power/Nodes/CableNode.cs +++ b/Content.Server/Power/Nodes/CableNode.cs @@ -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; diff --git a/Content.Server/Power/Nodes/CableTerminalNode.cs b/Content.Server/Power/Nodes/CableTerminalNode.cs index bb44e86256..8988a9950b 100644 --- a/Content.Server/Power/Nodes/CableTerminalNode.cs +++ b/Content.Server/Power/Nodes/CableTerminalNode.cs @@ -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; diff --git a/Content.Server/Power/Nodes/CableTerminalPortNode.cs b/Content.Server/Power/Nodes/CableTerminalPortNode.cs index bbce1aff74..f71f1c4aa6 100644 --- a/Content.Server/Power/Nodes/CableTerminalPortNode.cs +++ b/Content.Server/Power/Nodes/CableTerminalPortNode.cs @@ -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; diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenAutoCabling.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenAutoCabling.cs index aaea23ddd5..1ff28719fc 100644 --- a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenAutoCabling.cs +++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenAutoCabling.cs @@ -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; diff --git a/Content.Server/Sandbox/Commands/ColorNetworkCommand.cs b/Content.Server/Sandbox/Commands/ColorNetworkCommand.cs index 0a778f5880..2fcfcd8cbb 100644 --- a/Content.Server/Sandbox/Commands/ColorNetworkCommand.cs +++ b/Content.Server/Sandbox/Commands/ColorNetworkCommand.cs @@ -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 diff --git a/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs b/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs index bdd775c792..f8027b2f14 100644 --- a/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs +++ b/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs @@ -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; diff --git a/Content.Shared/Alert/AlertsSystem.cs b/Content.Shared/Alert/AlertsSystem.cs index c8f2a8e6bc..429c0670ad 100644 --- a/Content.Shared/Alert/AlertsSystem.cs +++ b/Content.Shared/Alert/AlertsSystem.cs @@ -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, 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 index 0000000000..b511258ff8 --- /dev/null +++ b/Content.Shared/Atmos/Components/BreathToolComponent.cs @@ -0,0 +1,28 @@ +using Content.Shared.Body.Components; +using Content.Shared.Inventory; +using Robust.Shared.GameStates; + +namespace Content.Shared.Atmos.Components; + +/// +/// Gas masks or the likes; used by for breathing. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[ComponentProtoName("BreathMask")] +public sealed partial class BreathToolComponent : Component +{ + /// + /// Tool is functional only in allowed slots + /// + [DataField] + public SlotFlags AllowedSlots = SlotFlags.MASK | SlotFlags.HEAD; + + [ViewVariables] + public bool IsFunctional => ConnectedInternalsEntity != null; + + /// + /// Entity that the breath tool is currently connected to. + /// + [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 index 0000000000..4214913f38 --- /dev/null +++ b/Content.Shared/Atmos/Components/GasTankComponent.cs @@ -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(); + + /// + /// Pressure at which tank should be considered 'low' such as for internals. + /// + [DataField] + public float TankLowPressure = DefaultLowPressure; + + /// + /// Distributed pressure. + /// + [DataField] + public float OutputPressure = DefaultOutputPressure; + + /// + /// The maximum allowed output pressure. + /// + [DataField] + public float MaxOutputPressure = 3 * DefaultOutputPressure; + + /// + /// Tank is connected to internals. + /// + [ViewVariables] + public bool IsConnected => User != null; + + [DataField, AutoNetworkedField] + public EntityUid? User; + + /// + /// 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. + /// + [ViewVariables] + public bool CheckUser; + + /// + /// Pressure at which tanks start leaking. + /// + [DataField] + public float TankLeakPressure = 30 * Atmospherics.OneAtmosphere; + + /// + /// Pressure at which tank spills all contents into atmosphere. + /// + [DataField] + public float TankRupturePressure = 40 * Atmospherics.OneAtmosphere; + + /// + /// Base 3x3 explosion. + /// + [DataField] + public float TankFragmentPressure = 50 * Atmospherics.OneAtmosphere; + + /// + /// Increases explosion for each scale kPa above threshold. + /// + [DataField] + public float TankFragmentScale = 2 * Atmospherics.OneAtmosphere; + + [DataField] + public EntProtoId ToggleAction = "ActionToggleInternals"; + + [DataField, AutoNetworkedField] + public EntityUid? ToggleActionEntity; + + /// + /// Valve to release gas from tank + /// + [DataField, AutoNetworkedField] + public bool IsValveOpen; + + /// + /// Gas release rate in L/s + /// + [DataField, AutoNetworkedField] + public float ValveOutputRate = 100f; + + [DataField] + public SoundSpecifier ValveSound = + new SoundCollectionSpecifier("valveSqueak") + { + Params = AudioParams.Default.WithVolume(-5f), + }; +} diff --git a/Content.Shared/Atmos/Components/SharedGasTankComponent.cs b/Content.Shared/Atmos/Components/SharedGasTankComponent.cs index f6751a3ebc..536d040766 100644 --- a/Content.Shared/Atmos/Components/SharedGasTankComponent.cs +++ b/Content.Shared/Atmos/Components/SharedGasTankComponent.cs @@ -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 index 0000000000..26b4d1949f --- /dev/null +++ b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.BreathTool.cs @@ -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(OnBreathToolShutdown); + SubscribeLocalEvent(OnMaskToggled); + } + + private void OnBreathToolShutdown(Entity entity, ref ComponentShutdown args) + { + DisconnectInternals(entity); + } + + public void DisconnectInternals(Entity 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 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); + } + } + } +} diff --git a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs index ced9cdcfe8..4698939734 100644 --- a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs +++ b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs @@ -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 _internalsQuery; protected readonly GasPrototype[] GasPrototypes = new GasPrototype[Atmospherics.TotalNumberOfGases]; @@ -14,6 +19,10 @@ namespace Content.Shared.Atmos.EntitySystems { base.Initialize(); + _internalsQuery = GetEntityQuery(); + + InitializeBreathTool(); + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) { GasPrototypes[i] = _prototypeManager.Index(i.ToString()); diff --git a/Content.Shared/Atmos/EntitySystems/SharedGasTankSystem.cs b/Content.Shared/Atmos/EntitySystems/SharedGasTankSystem.cs new file mode 100644 index 0000000000..27c3d16f29 --- /dev/null +++ b/Content.Shared/Atmos/EntitySystems/SharedGasTankSystem.cs @@ -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(OnGasShutdown); + SubscribeLocalEvent(BeforeUiOpen); + SubscribeLocalEvent(OnGetActions); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnActionToggle); + SubscribeLocalEvent(OnGasTankSetPressure); + SubscribeLocalEvent(OnGasTankToggleInternals); + SubscribeLocalEvent>(OnGetAlternativeVerb); + } + + private void OnGasShutdown(Entity gasTank, ref ComponentShutdown args) + { + DisconnectFromInternals(gasTank); + } + + private void OnGasTankToggleInternals(Entity ent, ref GasTankToggleInternalsMessage args) + { + ToggleInternals(ent, args.Actor); + } + + private void OnGasTankSetPressure(Entity 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 ent) + { + + } + + private void BeforeUiOpen(Entity 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 gasTank, ref ToggleActionEvent args) + { + if (args.Handled) + return; + + ToggleInternals(gasTank, user: args.Performer); + args.Handled = true; + } + + private void OnGetAlternativeVerb(EntityUid uid, GasTankComponent component, GetVerbsEvent 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 ent) + { + TryGetInternalsComp(ent, out _, out var internalsComp, ent.Comp.User); + return internalsComp != null && internalsComp.BreathTools.Count != 0 && !ent.Comp.IsValveOpen; + } + + public bool ConnectToInternals(Entity 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; + } + + /// + /// Tries to retrieve the internals component of either the gas tank's user, + /// or the gas tank's... containing container + /// + /// The user of the gas tank + /// True if internals comp isn't null, false if it is null + private bool TryGetInternalsComp(Entity 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(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(container.Owner, out var containerInternalsComp)) + { + internalsUid = container.Owner; + internalsComp = containerInternalsComp; + return true; + } + } + + return false; + } + + public bool DisconnectFromInternals(Entity 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 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 index 0000000000..5ad100319d --- /dev/null +++ b/Content.Shared/Atmos/IGasMixtureHolder.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.Atmos; + +public interface IGasMixtureHolder +{ + public GasMixture Air { get; set; } +} \ No newline at end of file diff --git a/Content.Shared/Atmos/Piping/Binary/Components/SharedGasCanisterComponent.cs b/Content.Shared/Atmos/Piping/Binary/Components/SharedGasCanisterComponent.cs index 1203639e09..6ad576fd8c 100644 --- a/Content.Shared/Atmos/Piping/Binary/Components/SharedGasCanisterComponent.cs +++ b/Content.Shared/Atmos/Piping/Binary/Components/SharedGasCanisterComponent.cs @@ -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. /// [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 index 0000000000..204cbc76d5 --- /dev/null +++ b/Content.Shared/Atmos/Piping/Unary/Components/GasCanisterComponent.cs @@ -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"; + + /// + /// Container name for the gas tank holder. + /// + [DataField("container")] + public string ContainerName { get; set; } = "tank_slot"; + + [DataField] + public ItemSlot GasTankSlot = new(); + + [DataField("gasMixture")] + public GasMixture Air { get; set; } = new(); + + /// + /// Last recorded pressure, for appearance-updating purposes. + /// + public float LastPressure = 0f; + + /// + /// Minimum release pressure possible for the release valve. + /// + [DataField, AutoNetworkedField] + public float MinReleasePressure = Atmospherics.OneAtmosphere / 10; + + /// + /// Maximum release pressure possible for the release valve. + /// + [DataField, AutoNetworkedField] + public float MaxReleasePressure = Atmospherics.OneAtmosphere * 10; + + /// + /// Valve release pressure. + /// + [DataField, AutoNetworkedField] + public float ReleasePressure = Atmospherics.OneAtmosphere; + + /// + /// Whether the release valve is open on the canister. + /// + [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 index 0000000000..e7a8ed5131 --- /dev/null +++ b/Content.Shared/Atmos/Piping/Unary/Systems/SharedGasCanisterSystem.cs @@ -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(OnCanisterContainerModified); + SubscribeLocalEvent(OnCanisterContainerModified); + SubscribeLocalEvent(OnCanisterInsertAttempt); + SubscribeLocalEvent(OnCanisterStartup); + + // Bound UI subscriptions + SubscribeLocalEvent(OnHoldingTankEjectMessage); + SubscribeLocalEvent(OnCanisterChangeReleasePressure); + SubscribeLocalEvent(OnCanisterChangeReleaseValve); + } + + private void OnCanisterStartup(Entity 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 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(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(); + 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(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 index 0000000000..ff9c977059 --- /dev/null +++ b/Content.Shared/Body/Components/InternalsComponent.cs @@ -0,0 +1,27 @@ +using Content.Shared.Alert; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Body.Components; + +/// +/// Handles hooking up a mask (breathing tool) / gas tank together and allowing the Owner to breathe through it. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +public sealed partial class InternalsComponent : Component +{ + [DataField, AutoNetworkedField] + public EntityUid? GasTankEntity; + + [DataField, AutoNetworkedField] + public HashSet BreathTools = new(); + + /// + /// Toggle Internals delay when the target is not you. + /// + [DataField] + public TimeSpan Delay = TimeSpan.FromSeconds(3); + + [DataField] + public ProtoId InternalsAlert = "Internals"; +} diff --git a/Content.Shared/Body/Systems/SharedInternalsSystem.cs b/Content.Shared/Body/Systems/SharedInternalsSystem.cs new file mode 100644 index 0000000000..0924d7a99e --- /dev/null +++ b/Content.Shared/Body/Systems/SharedInternalsSystem.cs @@ -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; + +/// +/// Handles lung breathing with gas tanks for entities. +/// +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>(OnGetInteractionVerbs); + + SubscribeLocalEvent(OnInternalsStartup); + SubscribeLocalEvent(OnInternalsShutdown); + + SubscribeLocalEvent(OnDoAfter); + SubscribeLocalEvent(OnToggleInternalsAlert); + } + + private void OnGetInteractionVerbs( + Entity ent, + ref GetVerbsEvent 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 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 ent, ref InternalsDoAfterEvent args) + { + if (args.Cancelled || args.Handled) + return; + + ToggleInternals(ent, args.User, force: true, ent); + + args.Handled = true; + } + + private void OnToggleInternalsAlert(Entity ent, ref ToggleInternalsAlertEvent args) + { + if (args.Handled) + return; + + args.Handled |= ToggleInternals(ent, ent, false, internals: ent.Comp); + } + + private void OnInternalsStartup(Entity ent, ref ComponentStartup args) + { + _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent)); + } + + private void OnInternalsShutdown(Entity ent, ref ComponentShutdown args) + { + _alerts.ClearAlert(ent, ent.Comp.InternalsAlert); + } + + public void ConnectBreathTool(Entity 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 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 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 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(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(component.GasTankEntity, out var gasTank) + && gasTank.IsLowPressure) + { + return 0; + } + + return 1; + } + + public Entity? FindBestGasTank( + Entity 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(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(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; + } +} diff --git a/Content.Shared/Clothing/EntitySystems/MaskSystem.cs b/Content.Shared/Clothing/EntitySystems/MaskSystem.cs index 3e899f3dc3..4f89b111bd 100644 --- a/Content.Shared/Clothing/EntitySystems/MaskSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/MaskSystem.cs @@ -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 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); + } + + /// + /// Called after setting IsToggled, raises events and dirties. + /// + 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 ent, ref FoldedEvent args) diff --git a/Content.Shared/Lock/ActivatableUIRequiresLockComponent.cs b/Content.Shared/Lock/ActivatableUIRequiresLockComponent.cs index 7d701ffd87..a2a9d8c556 100644 --- a/Content.Shared/Lock/ActivatableUIRequiresLockComponent.cs +++ b/Content.Shared/Lock/ActivatableUIRequiresLockComponent.cs @@ -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 /// [DataField] public bool RequireLocked; + + /// + /// Sound to be played if an attempt is blocked. + /// + [DataField] + public SoundSpecifier? AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg"); } diff --git a/Content.Shared/Lock/LockSystem.cs b/Content.Shared/Lock/LockSystem.cs index cbeceaf9e8..444b3e28e6 100644 --- a/Content.Shared/Lock/LockSystem.cs +++ b/Content.Shared/Lock/LockSystem.cs @@ -40,7 +40,7 @@ public sealed class LockSystem : EntitySystem base.Initialize(); SubscribeLocalEvent(OnStartup); - SubscribeLocalEvent(OnActivated); + SubscribeLocalEvent(OnActivated, before: [typeof(ActivatableUISystem)]); SubscribeLocalEvent(OnStorageOpenAttempt); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent>(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 index 0000000000..a4ab5103d9 --- /dev/null +++ b/Content.Shared/NodeContainer/Node.cs @@ -0,0 +1,100 @@ +using Content.Shared.NodeContainer.NodeGroups; +using Robust.Shared.Map.Components; + +namespace Content.Shared.NodeContainer; + +/// +/// Organizes themselves into distinct s with other s +/// that they can "reach" and have the same . +/// +[ImplicitDataDefinitionForInheritors] +public abstract partial class Node +{ + /// + /// An ID used as a criteria for combining into groups. Determines which + /// implementation is used as a group, detailed in . + /// + [DataField("nodeGroupID")] + public NodeGroupID NodeGroupID { get; private set; } = NodeGroupID.Default; + + /// + /// The node group this node is a part of. + /// + [ViewVariables] public INodeGroup? NodeGroup; + + /// + /// The entity that owns this node via its . + /// + [ViewVariables] public EntityUid Owner { get; private set; } = default!; + + /// + /// If this node should be considered for connection by other nodes. + /// + 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(Owner); + return xform.Anchored; + } + + [DataField] + public bool NeedAnchored { get; private set; } = true; + + public virtual void OnAnchorStateChanged(IEntityManager entityManager, bool anchored) { } + + /// + /// Prevents a node from being used by other nodes while midway through removal. + /// + public bool Deleting; + + /// + /// All compatible nodes that are reachable by this node. + /// Effectively, active connections out of this node. + /// + public readonly HashSet ReachableNodes = new(); + + public int FloodGen; + public int UndirectGen; + public bool FlaggedForFlood; + public int NetId; + + /// + /// Name of this node on the owning . + /// + public string Name = default!; + + /// + /// Invoked when the owning is initialized. + /// + /// The owning entity. + public virtual void Initialize(EntityUid owner, IEntityManager entMan) + { + Owner = owner; + } + + /// + /// How this node will attempt to find other reachable s to group with. + /// Returns a set of s to consider grouping with. Should not return this current . + /// + /// + /// + /// The set of nodes returned can be asymmetrical + /// (meaning that it can return other nodes whose 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 . + /// + /// + public abstract IEnumerable GetReachableNodes(TransformComponent xform, + EntityQuery nodeQuery, + EntityQuery xformQuery, + MapGridComponent? grid, + IEntityManager entMan); +} diff --git a/Content.Shared/NodeContainer/NodeContainerComponent.cs b/Content.Shared/NodeContainer/NodeContainerComponent.cs new file mode 100644 index 0000000000..674537bb79 --- /dev/null +++ b/Content.Shared/NodeContainer/NodeContainerComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Utility; + +namespace Content.Shared.NodeContainer; + +/// +/// Creates and maintains a set of s. +/// +[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 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 index 0000000000..da52d39e58 --- /dev/null +++ b/Content.Shared/NodeContainer/NodeGroups/INodeGroup.cs @@ -0,0 +1,33 @@ +using System.Linq; + +namespace Content.Shared.NodeContainer.NodeGroups; + +/// +/// Maintains a collection of s, and performs operations requiring a list of +/// all connected s. +/// +public interface INodeGroup +{ + bool Remaking { get; } + + /// + /// The list of nodes currently in this group. + /// + IReadOnlyList Nodes { get; } + + void Create(NodeGroupID groupId); + + void Initialize(Node sourceNode, IEntityManager entMan); + + void RemoveNode(Node node); + + void LoadNodes(List groupNodes); + + // In theory, the SS13 curse ensures this method will never be called. + void AfterRemake(IEnumerable> newGroups); + + /// + /// Return any additional data to display for the node-visualizer debug overlay. + /// + 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 index 0000000000..214e9c50ce --- /dev/null +++ b/Content.Shared/NodeContainer/NodeGroups/NodeGroupID.cs @@ -0,0 +1,19 @@ +namespace Content.Shared.NodeContainer.NodeGroups; + +public enum NodeGroupID : byte +{ + Default, + HVPower, + MVPower, + Apc, + AMEngine, + Pipe, + WireNet, + + /// + /// Group used by the TEG. + /// + /// + /// + Teg, +} diff --git a/Content.Shared/Timing/UseDelaySystem.cs b/Content.Shared/Timing/UseDelaySystem.cs index c6a1818443..d02752e16b 100644 --- a/Content.Shared/Timing/UseDelaySystem.cs +++ b/Content.Shared/Timing/UseDelaySystem.cs @@ -90,7 +90,7 @@ public sealed class UseDelaySystem : EntitySystem /// public bool IsDelayed(Entity 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 /// /// /// - public bool TryGetDelayInfo(Entity ent, [NotNullWhen(true)] out UseDelayInfo? info, string id = DefaultId) + public bool TryGetDelayInfo(Entity 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); } diff --git a/Content.Shared/UserInterface/ActivatableUIRequiresAnchorSystem.cs b/Content.Shared/UserInterface/ActivatableUIRequiresAnchorSystem.cs index 1d453334e3..6aa5059633 100644 --- a/Content.Shared/UserInterface/ActivatableUIRequiresAnchorSystem.cs +++ b/Content.Shared/UserInterface/ActivatableUIRequiresAnchorSystem.cs @@ -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(); } } diff --git a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml index 7baf1f0b5d..0dd7db2432 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml @@ -18,6 +18,10 @@ slots: - Back - suitStorage + - type: UseDelay + delays: + gasTank: + length: 1.0 - type: ActivatableUI key: enum.SharedGasTankUiKey.Key - type: UserInterface diff --git a/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml b/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml index a9dde098f9..e304d21b87 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml @@ -35,6 +35,10 @@ interfaces: enum.SharedGasTankUiKey.Key: type: GasTankBoundUserInterface + - type: UseDelay + delays: + gasTank: + length: 1.0 - type: Clothing sprite: Objects/Tanks/Jetpacks/blue.rsi quickEquip: false diff --git a/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml b/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml index 33c4dfbe17..f454c84480 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml @@ -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: