diff --git a/Content.Server/Alert/Click/ToggleInternals.cs b/Content.Server/Alert/Click/ToggleInternals.cs new file mode 100644 index 0000000000..3fa7e525f4 --- /dev/null +++ b/Content.Server/Alert/Click/ToggleInternals.cs @@ -0,0 +1,53 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Server.Popups; +using Content.Server.Shuttles.Systems; +using Content.Shared.Alert; +using Content.Shared.Shuttles.Components; +using JetBrains.Annotations; +using Robust.Shared.Player; + +namespace Content.Server.Alert.Click; + +/// +/// Attempts to toggle the internals for a particular entity +/// +[UsedImplicitly] +[DataDefinition] +public sealed class ToggleInternals : IAlertClick +{ + public void AlertClicked(EntityUid player) + { + var entManager = IoCManager.Resolve(); + + if (!entManager.TryGetComponent(player, out var internals)) return; + + var popups = entManager.EntitySysManager.GetEntitySystem(); + var internalsSystem = entManager.EntitySysManager.GetEntitySystem(); + + // Toggle off if they're on + if (internalsSystem.AreInternalsWorking(internals)) + { + internalsSystem.DisconnectTank(internals); + return; + } + + // If they're not on then check if we have a mask to use + if (internals.BreathToolEntity == null) + { + popups.PopupEntity(Loc.GetString("internals-no-breath-tool"), player, Filter.Entities(player)); + return; + } + + var tank = internalsSystem.FindBestGasTank(internals); + + if (tank == null) + { + popups.PopupEntity(Loc.GetString("internals-no-tank"), player, Filter.Entities(player)); + return; + } + + entManager.EntitySysManager.GetEntitySystem().ConnectToInternals(tank); + } +} diff --git a/Content.Server/Atmos/Components/BreathToolComponent.cs b/Content.Server/Atmos/Components/BreathToolComponent.cs index 7f500ae5ae..edc494bb36 100644 --- a/Content.Server/Atmos/Components/BreathToolComponent.cs +++ b/Content.Server/Atmos/Components/BreathToolComponent.cs @@ -1,5 +1,4 @@ -using Content.Server.Body.Components; -using Content.Shared.Inventory; +using Content.Shared.Inventory; namespace Content.Server.Atmos.Components { @@ -10,33 +9,12 @@ namespace Content.Server.Atmos.Components [ComponentProtoName("BreathMask")] public sealed class BreathToolComponent : Component { - [Dependency] private readonly IEntityManager _entities = default!; - /// /// Tool is functional only in allowed slots /// [DataField("allowedSlots")] public SlotFlags AllowedSlots = SlotFlags.MASK; public bool IsFunctional; - public EntityUid ConnectedInternalsEntity; - - protected override void Shutdown() - { - base.Shutdown(); - DisconnectInternals(); - } - - public void DisconnectInternals() - { - var old = ConnectedInternalsEntity; - ConnectedInternalsEntity = default; - - if (old != default && _entities.TryGetComponent(old, out var internalsComponent)) - { - internalsComponent.DisconnectBreathTool(); - } - - IsFunctional = false; - } + public EntityUid? ConnectedInternalsEntity; } } diff --git a/Content.Server/Atmos/Components/GasTankComponent.cs b/Content.Server/Atmos/Components/GasTankComponent.cs index ddacdfd0b4..c4ec400a47 100644 --- a/Content.Server/Atmos/Components/GasTankComponent.cs +++ b/Content.Server/Atmos/Components/GasTankComponent.cs @@ -1,49 +1,35 @@ -using Content.Server.Atmos.EntitySystems; -using Content.Server.Body.Components; -using Content.Server.Explosion.EntitySystems; -using Content.Server.UserInterface; -using Content.Shared.Actions; using Content.Shared.Actions.ActionTypes; using Content.Shared.Atmos; -using Content.Shared.Atmos.Components; -using Content.Shared.Audio; using Content.Shared.Sound; -using Robust.Server.GameObjects; using Robust.Shared.Audio; -using Robust.Shared.Containers; -using Robust.Shared.Player; namespace Content.Server.Atmos.Components { [RegisterComponent] -#pragma warning disable 618 public sealed class GasTankComponent : Component, IGasMixtureHolder -#pragma warning restore 618 { - [Dependency] private readonly IEntityManager _entMan = default!; - - private const float MaxExplosionRange = 14f; + public const float MaxExplosionRange = 14f; private const float DefaultOutputPressure = Atmospherics.OneAtmosphere; - private int _integrity = 3; + public int Integrity = 3; - [ViewVariables] public BoundUserInterface? UserInterface; + [ViewVariables(VVAccess.ReadWrite), DataField("ruptureSound")] + public SoundSpecifier RuptureSound = new SoundPathSpecifier("/Audio/Effects/spray.ogg"); - [ViewVariables(VVAccess.ReadWrite), DataField("ruptureSound")] private SoundSpecifier _ruptureSound = new SoundPathSpecifier("/Audio/Effects/spray.ogg"); - - [ViewVariables(VVAccess.ReadWrite), DataField("connectSound")] private SoundSpecifier? _connectSound = + [ViewVariables(VVAccess.ReadWrite), DataField("connectSound")] + public SoundSpecifier? ConnectSound = new SoundPathSpecifier("/Audio/Effects/internals.ogg") { - Params = AudioParams.Default.WithVolume(10f), + Params = AudioParams.Default.WithVolume(5f), }; - [ViewVariables(VVAccess.ReadWrite), DataField("disconnectSound")] private SoundSpecifier? _disconnectSound; + [ViewVariables(VVAccess.ReadWrite), DataField("disconnectSound")] + public SoundSpecifier? DisconnectSound; // Cancel toggles sounds if we re-toggle again. - private IPlayingAudioStream? _connectStream; - private IPlayingAudioStream? _disconnectStream; - + public IPlayingAudioStream? ConnectStream; + public IPlayingAudioStream? DisconnectStream; [DataField("air")] [ViewVariables] public GasMixture Air { get; set; } = new(); @@ -52,18 +38,13 @@ namespace Content.Server.Atmos.Components /// [DataField("outputPressure")] [ViewVariables] - public float OutputPressure { get; private set; } = DefaultOutputPressure; + public float OutputPressure { get; set; } = DefaultOutputPressure; /// /// Tank is connected to internals. /// [ViewVariables] public bool IsConnected { get; set; } - /// - /// Represents that tank is functional and can be connected to internals. - /// - public bool IsFunctional => GetInternalsComponent() != null; - /// /// Pressure at which tanks start leaking. /// @@ -90,212 +71,5 @@ namespace Content.Server.Atmos.Components [DataField("toggleAction", required: true)] public InstantAction ToggleAction = new(); - - protected override void Initialize() - { - base.Initialize(); - UserInterface = Owner.GetUIOrNull(SharedGasTankUiKey.Key); - if (UserInterface != null) - { - UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage; - } - } - - protected override void Shutdown() - { - base.Shutdown(); - DisconnectFromInternals(); - } - - public GasMixture? RemoveAir(float amount) - { - var gas = Air?.Remove(amount); - CheckStatus(); - return gas; - } - - public GasMixture RemoveAirVolume(float volume) - { - if (Air == null) - return new GasMixture(volume); - - var tankPressure = Air.Pressure; - if (tankPressure < OutputPressure) - { - OutputPressure = tankPressure; - UpdateUserInterface(); - } - - var molesNeeded = OutputPressure * volume / (Atmospherics.R * Air.Temperature); - - var air = RemoveAir(molesNeeded); - - if (air != null) - air.Volume = volume; - else - return new GasMixture(volume); - - return air; - } - - public void ConnectToInternals() - { - if (IsConnected || !IsFunctional) return; - var internals = GetInternalsComponent(); - if (internals == null) return; - IsConnected = internals.TryConnectTank(Owner); - EntitySystem.Get().SetToggled(ToggleAction, IsConnected); - - // Couldn't toggle! - if (!IsConnected) return; - - _connectStream?.Stop(); - - if (_connectSound != null) - _connectStream = SoundSystem.Play(_connectSound.GetSound(), Filter.Pvs(Owner, entityManager: _entMan), Owner, _connectSound.Params); - - UpdateUserInterface(); - } - - public void DisconnectFromInternals(EntityUid? owner = null) - { - if (!IsConnected) return; - IsConnected = false; - EntitySystem.Get().SetToggled(ToggleAction, false); - - GetInternalsComponent(owner)?.DisconnectTank(); - _disconnectStream?.Stop(); - - if (_disconnectSound != null) - _disconnectStream = SoundSystem.Play(_disconnectSound.GetSound(), Filter.Pvs(Owner, entityManager: _entMan), Owner, _disconnectSound.Params); - - UpdateUserInterface(); - } - - public void UpdateUserInterface(bool initialUpdate = false) - { - var internals = GetInternalsComponent(); - UserInterface?.SetState( - new GasTankBoundUserInterfaceState - { - TankPressure = Air?.Pressure ?? 0, - OutputPressure = initialUpdate ? OutputPressure : null, - InternalsConnected = IsConnected, - CanConnectInternals = IsFunctional && internals != null - }); - } - - private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message) - { - switch (message.Message) - { - case GasTankSetPressureMessage msg: - OutputPressure = msg.Pressure; - break; - case GasTankToggleInternalsMessage _: - ToggleInternals(); - break; - } - } - - internal void ToggleInternals() - { - if (IsConnected) - { - DisconnectFromInternals(); - return; - } - - ConnectToInternals(); - } - - private InternalsComponent? GetInternalsComponent(EntityUid? owner = null) - { - if (_entMan.Deleted(Owner)) return null; - if (owner != null) return _entMan.GetComponentOrNull(owner.Value); - return Owner.TryGetContainer(out var container) - ? _entMan.GetComponentOrNull(container.Owner) - : null; - } - - public void AssumeAir(GasMixture giver) - { - var atmos = EntitySystem.Get(); - atmos.Merge(Air, giver); - CheckStatus(atmos); - } - - public void CheckStatus(AtmosphereSystem? atmosphereSystem=null) - { - if (Air == null) - return; - - atmosphereSystem ??= EntitySystem.Get(); - - var pressure = Air.Pressure; - - if (pressure > TankFragmentPressure) - { - // Give the gas a chance to build up more pressure. - for (var i = 0; i < 3; i++) - { - atmosphereSystem.React(Air, this); - } - - pressure = Air.Pressure; - var range = (pressure - TankFragmentPressure) / TankFragmentScale; - - // Let's cap the explosion, yeah? - // !1984 - if (range > MaxExplosionRange) - { - range = MaxExplosionRange; - } - - EntitySystem.Get().TriggerExplosive(Owner, radius: range); - - return; - } - - if (pressure > TankRupturePressure) - { - if (_integrity <= 0) - { - var environment = atmosphereSystem.GetContainingMixture(Owner, false, true); - if(environment != null) - atmosphereSystem.Merge(environment, Air); - - SoundSystem.Play(_ruptureSound.GetSound(), Filter.Pvs(Owner), _entMan.GetComponent(Owner).Coordinates, AudioHelpers.WithVariation(0.125f)); - - _entMan.QueueDeleteEntity(Owner); - return; - } - - _integrity--; - return; - } - - if (pressure > TankLeakPressure) - { - if (_integrity <= 0) - { - var environment = atmosphereSystem.GetContainingMixture(Owner, false, true); - if (environment == null) - return; - - var leakedGas = Air.RemoveRatio(0.25f); - atmosphereSystem.Merge(environment, leakedGas); - } - else - { - _integrity--; - } - - return; - } - - if (_integrity < 3) - _integrity++; - } } } diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BreathTool.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BreathTool.cs new file mode 100644 index 0000000000..22270cb541 --- /dev/null +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BreathTool.cs @@ -0,0 +1,30 @@ +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(EntityUid uid, BreathToolComponent component, ComponentShutdown args) + { + DisconnectInternals(component); + } + + public void DisconnectInternals(BreathToolComponent component) + { + var old = component.ConnectedInternalsEntity; + component.ConnectedInternalsEntity = null; + + if (TryComp(old, out var internalsComponent)) + { + _internals.DisconnectBreathTool(internalsComponent); + } + + component.IsFunctional = false; + } +} diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs index 5089090e12..901154ba22 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Administration.Logs; using Content.Server.Atmos.Components; +using Content.Server.Body.Systems; using Content.Server.NodeContainer.EntitySystems; using Content.Shared.Atmos.EntitySystems; using Content.Shared.Maps; @@ -21,6 +22,7 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly IAdminLogManager _adminLog = 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!; @@ -36,6 +38,7 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem UpdatesAfter.Add(typeof(NodeGroupSystem)); + InitializeBreathTool(); InitializeGases(); InitializeCommands(); InitializeCVars(); diff --git a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs index b8412c14dd..309a508d97 100644 --- a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs @@ -1,10 +1,22 @@ using Content.Server.Atmos.Components; +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Server.Explosion.EntitySystems; using Content.Server.UserInterface; using Content.Shared.Actions; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Audio; using Content.Shared.Interaction.Events; using Content.Shared.Toggleable; using Content.Shared.Examine; +using Content.Shared.Inventory; using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Server.Player; +using Robust.Shared.Audio; +using Robust.Shared.Containers; +using Robust.Shared.Player; namespace Content.Server.Atmos.EntitySystems { @@ -12,6 +24,11 @@ namespace Content.Server.Atmos.EntitySystems public sealed class GasTankSystem : EntitySystem { [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly ExplosionSystem _explosions = default!; + [Dependency] private readonly InternalsSystem _internals = default!; + [Dependency] private readonly SharedContainerSystem _containers = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly UserInterfaceSystem _ui = default!; private const float TimerDelay = 0.5f; private float _timer = 0f; @@ -19,22 +36,57 @@ namespace Content.Server.Atmos.EntitySystems public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnGasShutdown); SubscribeLocalEvent(BeforeUiOpen); SubscribeLocalEvent(OnGetActions); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnActionToggle); SubscribeLocalEvent(OnDropped); + + SubscribeLocalEvent(OnGasTankSetPressure); + SubscribeLocalEvent(OnGasTankToggleInternals); + } + + private void OnGasShutdown(EntityUid uid, GasTankComponent component, ComponentShutdown args) + { + DisconnectFromInternals(component); + } + + private void OnGasTankToggleInternals(EntityUid uid, GasTankComponent component, GasTankToggleInternalsMessage args) + { + if (args.Session is not IPlayerSession playerSession || + playerSession.AttachedEntity is not {} player) return; + + ToggleInternals(component); + } + + private void OnGasTankSetPressure(EntityUid uid, GasTankComponent component, GasTankSetPressureMessage args) + { + component.OutputPressure = args.Pressure; + } + + public void UpdateUserInterface(GasTankComponent component, bool initialUpdate = false) + { + var internals = GetInternalsComponent(component); + _ui.GetUiOrNull(component.Owner, SharedGasTankUiKey.Key)?.SetState( + new GasTankBoundUserInterfaceState + { + TankPressure = component.Air?.Pressure ?? 0, + OutputPressure = initialUpdate ? component.OutputPressure : null, + InternalsConnected = component.IsConnected, + CanConnectInternals = IsFunctional(component) && internals != null + }); } private void BeforeUiOpen(EntityUid uid, GasTankComponent component, BeforeActivatableUIOpenEvent args) { // Only initial update includes output pressure information, to avoid overwriting client-input as the updates come in. - component.UpdateUserInterface(true); + UpdateUserInterface(component, true); } private void OnDropped(EntityUid uid, GasTankComponent component, DroppedEvent args) { - component.DisconnectFromInternals(args.User); + DisconnectFromInternals(component, args.User); } private void OnGetActions(EntityUid uid, GasTankComponent component, GetItemActionsEvent args) @@ -55,8 +107,7 @@ namespace Content.Server.Atmos.EntitySystems if (args.Handled) return; - component.ToggleInternals(); - + ToggleInternals(component); args.Handled = true; } @@ -72,13 +123,185 @@ namespace Content.Server.Atmos.EntitySystems foreach (var gasTank in EntityManager.EntityQuery()) { _atmosphereSystem.React(gasTank.Air, gasTank); - gasTank.CheckStatus(_atmosphereSystem); - - if (gasTank.UserInterface != null && gasTank.UserInterface.SubscribedSessions.Count > 0) + CheckStatus(gasTank); + if (_ui.IsUiOpen(gasTank.Owner, SharedGasTankUiKey.Key)) { - gasTank.UpdateUserInterface(); + UpdateUserInterface(gasTank); } } } + + private void ToggleInternals(GasTankComponent component) + { + if (component.IsConnected) + { + DisconnectFromInternals(component); + } + else + { + ConnectToInternals(component); + } + } + + public GasMixture? RemoveAir(GasTankComponent component, float amount) + { + var gas = component.Air?.Remove(amount); + CheckStatus(component); + return gas; + } + + public GasMixture RemoveAirVolume(GasTankComponent component, float volume) + { + if (component.Air == null) + return new GasMixture(volume); + + var tankPressure = component.Air.Pressure; + if (tankPressure < component.OutputPressure) + { + component.OutputPressure = tankPressure; + UpdateUserInterface(component); + } + + var molesNeeded = component.OutputPressure * volume / (Atmospherics.R * component.Air.Temperature); + + var air = RemoveAir(component, molesNeeded); + + if (air != null) + air.Volume = volume; + else + return new GasMixture(volume); + + return air; + } + + public bool CanConnectToInternals(GasTankComponent component) + { + return !component.IsConnected && IsFunctional(component); + } + + public void ConnectToInternals(GasTankComponent component) + { + if (!CanConnectToInternals(component)) return; + var internals = GetInternalsComponent(component); + if (internals == null) return; + component.IsConnected = _internals.TryConnectTank(internals, component.Owner); + _actions.SetToggled(component.ToggleAction, component.IsConnected); + + // Couldn't toggle! + if (!component.IsConnected) return; + + component.ConnectStream?.Stop(); + + if (component.ConnectSound != null) + component.ConnectStream = SoundSystem.Play(component.ConnectSound.GetSound(), Filter.Pvs(component.Owner, entityManager: EntityManager), component.Owner, component.ConnectSound.Params); + + UpdateUserInterface(component); + } + + public void DisconnectFromInternals(GasTankComponent component, EntityUid? owner = null) + { + if (!component.IsConnected) return; + component.IsConnected = false; + _actions.SetToggled(component.ToggleAction, false); + + _internals.DisconnectTank(GetInternalsComponent(component, owner)); + component.DisconnectStream?.Stop(); + + if (component.DisconnectSound != null) + component.DisconnectStream = SoundSystem.Play(component.DisconnectSound.GetSound(), Filter.Pvs(component.Owner, entityManager: EntityManager), component.Owner, component.DisconnectSound.Params); + + UpdateUserInterface(component); + } + + private InternalsComponent? GetInternalsComponent(GasTankComponent component, EntityUid? owner = null) + { + if (Deleted(component.Owner)) return null; + if (owner != null) return CompOrNull(owner.Value); + return _containers.TryGetContainingContainer(component.Owner, out var container) + ? CompOrNull(container.Owner) + : null; + } + + public void AssumeAir(GasTankComponent component, GasMixture giver) + { + _atmosphereSystem.Merge(component.Air, giver); + CheckStatus(component); + } + + public void CheckStatus(GasTankComponent component) + { + if (component.Air == null) + return; + + var pressure = component.Air.Pressure; + + if (pressure > component.TankFragmentPressure) + { + // Give the gas a chance to build up more pressure. + for (var i = 0; i < 3; i++) + { + _atmosphereSystem.React(component.Air, component); + } + + pressure = component.Air.Pressure; + var range = (pressure - component.TankFragmentPressure) / component.TankFragmentScale; + + // Let's cap the explosion, yeah? + // !1984 + if (range > GasTankComponent.MaxExplosionRange) + { + range = GasTankComponent.MaxExplosionRange; + } + + _explosions.TriggerExplosive(component.Owner, radius: range); + + return; + } + + if (pressure > component.TankRupturePressure) + { + if (component.Integrity <= 0) + { + var environment = _atmosphereSystem.GetContainingMixture(component.Owner, false, true); + if(environment != null) + _atmosphereSystem.Merge(environment, component.Air); + + SoundSystem.Play(component.RuptureSound.GetSound(), Filter.Pvs(component.Owner), Transform(component.Owner).Coordinates, AudioHelpers.WithVariation(0.125f)); + + QueueDel(component.Owner); + return; + } + + component.Integrity--; + return; + } + + if (pressure > component.TankLeakPressure) + { + if (component.Integrity <= 0) + { + var environment = _atmosphereSystem.GetContainingMixture(component.Owner, false, true); + if (environment == null) + return; + + var leakedGas = component.Air.RemoveRatio(0.25f); + _atmosphereSystem.Merge(environment, leakedGas); + } + else + { + component.Integrity--; + } + + return; + } + + if (component.Integrity < 3) + component.Integrity++; + } + + private bool IsFunctional(GasTankComponent component) + { + return GetInternalsComponent(component) != null; + } } } diff --git a/Content.Server/Body/Components/InternalsComponent.cs b/Content.Server/Body/Components/InternalsComponent.cs index b1c50765e1..faef3d32b9 100644 --- a/Content.Server/Body/Components/InternalsComponent.cs +++ b/Content.Server/Body/Components/InternalsComponent.cs @@ -1,68 +1,12 @@ -using Content.Server.Atmos.Components; - -namespace Content.Server.Body.Components +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 class InternalsComponent : Component { - [Dependency] private readonly IEntityManager _entMan = default!; - [ViewVariables] public EntityUid? GasTankEntity { get; set; } [ViewVariables] public EntityUid? BreathToolEntity { get; set; } - - public void DisconnectBreathTool() - { - var old = BreathToolEntity; - BreathToolEntity = null; - - if (_entMan.TryGetComponent(old, out BreathToolComponent? breathTool) ) - { - breathTool.DisconnectInternals(); - DisconnectTank(); - } - } - - public void ConnectBreathTool(EntityUid toolEntity) - { - if (_entMan.TryGetComponent(BreathToolEntity, out BreathToolComponent? tool)) - { - tool.DisconnectInternals(); - } - - BreathToolEntity = toolEntity; - } - - public void DisconnectTank() - { - if (_entMan.TryGetComponent(GasTankEntity, out GasTankComponent? tank)) - { - tank.DisconnectFromInternals(Owner); - } - - GasTankEntity = null; - } - - public bool TryConnectTank(EntityUid tankEntity) - { - if (BreathToolEntity == null) - return false; - - if (_entMan.TryGetComponent(GasTankEntity, out GasTankComponent? tank)) - { - tank.DisconnectFromInternals(Owner); - } - - GasTankEntity = tankEntity; - return true; - } - - public bool AreInternalsWorking() - { - return _entMan.TryGetComponent(BreathToolEntity, out BreathToolComponent? breathTool) && - breathTool.IsFunctional && - _entMan.TryGetComponent(GasTankEntity, out GasTankComponent? gasTank) && - gasTank.Air != null; - } - } } diff --git a/Content.Server/Body/Systems/InternalsSystem.cs b/Content.Server/Body/Systems/InternalsSystem.cs index 5a5076eb92..86825b4500 100644 --- a/Content.Server/Body/Systems/InternalsSystem.cs +++ b/Content.Server/Body/Systems/InternalsSystem.cs @@ -1,24 +1,175 @@ using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; using Content.Server.Body.Components; +using Content.Server.Hands.Systems; +using Content.Shared.Alert; using Content.Shared.Atmos; +using Content.Shared.Inventory; +using Robust.Shared.Containers; +using Robust.Shared.Prototypes; namespace Content.Server.Body.Systems; public sealed class InternalsSystem : EntitySystem { + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly AtmosphereSystem _atmos = default!; + [Dependency] private readonly GasTankSystem _gasTank = default!; + [Dependency] private readonly HandsSystem _hands = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnInhaleLocation); + SubscribeLocalEvent(OnInternalsStartup); + SubscribeLocalEvent(OnInternalsShutdown); + } + + private void OnInternalsStartup(EntityUid uid, InternalsComponent component, ComponentStartup args) + { + _alerts.ShowAlert(component.Owner, AlertType.Internals, GetSeverity(component)); + } + + private void OnInternalsShutdown(EntityUid uid, InternalsComponent component, ComponentShutdown args) + { + _alerts.ClearAlert(uid, AlertType.Internals); } private void OnInhaleLocation(EntityUid uid, InternalsComponent component, InhaleLocationEvent args) { - if (component.AreInternalsWorking()) + if (AreInternalsWorking(component)) { var gasTank = Comp(component.GasTankEntity!.Value); - args.Gas = gasTank.RemoveAirVolume(Atmospherics.BreathVolume); + args.Gas = _gasTank.RemoveAirVolume(gasTank, Atmospherics.BreathVolume); + // TODO: Should listen to gas tank updates instead I guess? + _alerts.ShowAlert(component.Owner, AlertType.Internals, GetSeverity(component)); } } + public void DisconnectBreathTool(InternalsComponent component) + { + var old = component.BreathToolEntity; + component.BreathToolEntity = null; + + if (TryComp(old, out BreathToolComponent? breathTool) ) + { + _atmos.DisconnectInternals(breathTool); + DisconnectTank(component); + } + + _alerts.ShowAlert(component.Owner, AlertType.Internals, GetSeverity(component)); + } + + public void ConnectBreathTool(InternalsComponent component, EntityUid toolEntity) + { + if (TryComp(component.BreathToolEntity, out BreathToolComponent? tool)) + { + _atmos.DisconnectInternals(tool); + } + + component.BreathToolEntity = toolEntity; + _alerts.ShowAlert(component.Owner, AlertType.Internals, GetSeverity(component)); + } + + public void DisconnectTank(InternalsComponent? component) + { + if (component == null) return; + + if (TryComp(component.GasTankEntity, out GasTankComponent? tank)) + { + _gasTank.DisconnectFromInternals(tank, component.Owner); + } + + component.GasTankEntity = null; + _alerts.ShowAlert(component.Owner, AlertType.Internals, GetSeverity(component)); + } + + public bool TryConnectTank(InternalsComponent component, EntityUid tankEntity) + { + if (component.BreathToolEntity == null) + return false; + + if (TryComp(component.GasTankEntity, out GasTankComponent? tank)) + { + _gasTank.DisconnectFromInternals(tank, component.Owner); + } + + component.GasTankEntity = tankEntity; + _alerts.ShowAlert(component.Owner, AlertType.Internals, GetSeverity(component)); + return true; + } + + public bool AreInternalsWorking(InternalsComponent component) + { + return TryComp(component.BreathToolEntity, out BreathToolComponent? breathTool) && + breathTool.IsFunctional && + TryComp(component.GasTankEntity, out GasTankComponent? gasTank) && + gasTank.Air != null; + } + + private short GetSeverity(InternalsComponent component) + { + if (component.BreathToolEntity == null || !AreInternalsWorking(component)) return 2; + + if (TryComp(component.GasTankEntity, out var gasTank) && gasTank.Air.Volume < Atmospherics.BreathVolume) + return 0; + + return 1; + } + + public GasTankComponent? FindBestGasTank(InternalsComponent component) + { + // Prioritise + // 1. exo-slot tanks + // 2. in-hand tanks + // 3. pocket tanks + InventoryComponent? inventory = null; + ContainerManagerComponent? containerManager = null; + + if (_inventory.TryGetSlotEntity(component.Owner, "suitstorage", out var entity, inventory, containerManager) && + TryComp(entity, out var gasTank) && + _gasTank.CanConnectToInternals(gasTank)) + { + return gasTank; + } + + var tanks = new List(); + + foreach (var hand in _hands.EnumerateHands(component.Owner)) + { + if (TryComp(hand.HeldEntity, out gasTank) && _gasTank.CanConnectToInternals(gasTank)) + { + tanks.Add(gasTank); + } + } + + if (tanks.Count > 0) + { + tanks.Sort((x, y) => y.Air.TotalMoles.CompareTo(x.Air.TotalMoles)); + return tanks[0]; + } + + if (Resolve(component.Owner, ref inventory, false)) + { + var enumerator = new InventorySystem.ContainerSlotEnumerator(component.Owner, inventory.TemplateId, _protoManager, _inventory, SlotFlags.POCKET); + + while (enumerator.MoveNext(out var container)) + { + if (TryComp(container.ContainedEntity, out gasTank) && _gasTank.CanConnectToInternals(gasTank)) + { + tanks.Add(gasTank); + } + } + + if (tanks.Count > 0) + { + tanks.Sort((x, y) => y.Air.TotalMoles.CompareTo(x.Air.TotalMoles)); + return tanks[0]; + } + } + + return null; + } } diff --git a/Content.Server/Body/Systems/LungSystem.cs b/Content.Server/Body/Systems/LungSystem.cs index 0e995c78d1..042c90857b 100644 --- a/Content.Server/Body/Systems/LungSystem.cs +++ b/Content.Server/Body/Systems/LungSystem.cs @@ -9,6 +9,7 @@ namespace Content.Server.Body.Systems; public sealed class LungSystem : EntitySystem { + [Dependency] private readonly InternalsSystem _internals = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; @@ -24,7 +25,7 @@ public sealed class LungSystem : EntitySystem private void OnGotUnequipped(EntityUid uid, BreathToolComponent component, GotUnequippedEvent args) { - component.DisconnectInternals(); + _atmosphereSystem.DisconnectInternals(component); } private void OnGotEquipped(EntityUid uid, BreathToolComponent component, GotEquippedEvent args) @@ -36,7 +37,7 @@ public sealed class LungSystem : EntitySystem if (TryComp(args.Equipee, out InternalsComponent? internals)) { component.ConnectedInternalsEntity = args.Equipee; - internals.ConnectBreathTool(uid); + _internals.ConnectBreathTool(internals, uid); } } diff --git a/Content.Server/Chemistry/Components/SmokeSolutionAreaEffectComponent.cs b/Content.Server/Chemistry/Components/SmokeSolutionAreaEffectComponent.cs index 0771ba447c..631de97ce5 100644 --- a/Content.Server/Chemistry/Components/SmokeSolutionAreaEffectComponent.cs +++ b/Content.Server/Chemistry/Components/SmokeSolutionAreaEffectComponent.cs @@ -34,7 +34,7 @@ namespace Content.Server.Chemistry.Components return; if (_entMan.TryGetComponent(entity, out InternalsComponent? internals) && - internals.AreInternalsWorking()) + IoCManager.Resolve().GetEntitySystem().AreInternalsWorking(internals)) return; var chemistry = EntitySystem.Get(); diff --git a/Content.Server/Clothing/MaskSystem.cs b/Content.Server/Clothing/MaskSystem.cs index 202d905e50..b03787288e 100644 --- a/Content.Server/Clothing/MaskSystem.cs +++ b/Content.Server/Clothing/MaskSystem.cs @@ -5,7 +5,9 @@ using Content.Shared.Inventory.Events; using Content.Shared.Item; using Content.Server.Actions; using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; using Content.Server.Body.Components; +using Content.Server.Body.Systems; using Content.Server.Clothing.Components; using Content.Server.Disease.Components; using Content.Server.IdentityManagement; @@ -18,9 +20,11 @@ namespace Content.Server.Clothing { public sealed class MaskSystem : EntitySystem { - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly InventorySystem _inventorySystem = default!; [Dependency] private readonly ActionsSystem _actionSystem = default!; + [Dependency] private readonly AtmosphereSystem _atmos = default!; + [Dependency] private readonly InternalsSystem _internals = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly IdentitySystem _identity = default!; public override void Initialize() @@ -100,7 +104,7 @@ namespace Content.Server.Clothing if (mask.IsToggled) { - breathTool.DisconnectInternals(); + _atmos.DisconnectInternals(breathTool); } else { @@ -109,7 +113,7 @@ namespace Content.Server.Clothing if (TryComp(wearer, out InternalsComponent? internals)) { breathTool.ConnectedInternalsEntity = wearer; - internals.ConnectBreathTool(uid); + _internals.ConnectBreathTool(internals, uid); } } } diff --git a/Content.Server/Movement/Systems/JetpackSystem.cs b/Content.Server/Movement/Systems/JetpackSystem.cs index 7c15621992..f1e7a91aac 100644 --- a/Content.Server/Movement/Systems/JetpackSystem.cs +++ b/Content.Server/Movement/Systems/JetpackSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Robust.Shared.Collections; @@ -7,6 +8,8 @@ namespace Content.Server.Movement.Systems; public sealed class JetpackSystem : SharedJetpackSystem { + [Dependency] private readonly GasTankSystem _gasTank = default!; + private const float UpdateCooldown = 0.5f; protected override bool CanEnable(JetpackComponent component) @@ -26,7 +29,7 @@ public sealed class JetpackSystem : SharedJetpackSystem if (active.Accumulator < UpdateCooldown) continue; active.Accumulator -= UpdateCooldown; - var air = gasTank.RemoveAir(comp.MoleUsage); + var air = _gasTank.RemoveAir(gasTank, comp.MoleUsage); if (air == null || !MathHelper.CloseTo(air.TotalMoles, comp.MoleUsage, 0.001f)) { @@ -34,7 +37,7 @@ public sealed class JetpackSystem : SharedJetpackSystem continue; } - gasTank.UpdateUserInterface(); + _gasTank.UpdateUserInterface(gasTank); } foreach (var comp in toDisable) diff --git a/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs b/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs index 6bc3df18f5..dfd6c2db31 100644 --- a/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs +++ b/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs @@ -1,6 +1,7 @@ using System.Linq; using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; +using Content.Server.Camera; using Content.Server.Nutrition.Components; using Content.Server.Storage.EntitySystems; using Content.Server.Storage.Components; @@ -27,12 +28,13 @@ namespace Content.Server.PneumaticCannon public sealed class PneumaticCannonSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly StunSystem _stun = default!; [Dependency] private readonly AtmosphereSystem _atmos = default!; - [Dependency] private readonly SharedCameraRecoilSystem _sharedCameraRecoil = default!; + [Dependency] private readonly CameraRecoilSystem _cameraRecoil = default!; + [Dependency] private readonly GasTankSystem _gasTank = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; - [Dependency] private readonly ThrowingSystem _throwingSystem = default!; [Dependency] private readonly StorageSystem _storageSystem = default!; + [Dependency] private readonly StunSystem _stun = default!; + [Dependency] private readonly ThrowingSystem _throwingSystem = default!; private HashSet _currentlyFiring = new(); @@ -211,7 +213,7 @@ namespace Content.Server.PneumaticCannon { data.User.PopupMessage(Loc.GetString("pneumatic-cannon-component-fire-no-gas", ("cannon", comp.Owner))); - SoundSystem.Play("/Audio/Items/hiss.ogg", Filter.Pvs(((IComponent) comp).Owner), ((IComponent) comp).Owner, AudioParams.Default); + SoundSystem.Play("/Audio/Items/hiss.ogg", Filter.Pvs(comp.Owner), comp.Owner, AudioParams.Default); return; } @@ -227,11 +229,11 @@ namespace Content.Server.PneumaticCannon var ent = _random.Pick(storage.StoredEntities); _storageSystem.RemoveAndDrop(comp.Owner, ent, storage); - SoundSystem.Play(comp.FireSound.GetSound(), Filter.Pvs(data.User), ((IComponent) comp).Owner, AudioParams.Default); + SoundSystem.Play(comp.FireSound.GetSound(), Filter.Pvs(data.User), comp.Owner, AudioParams.Default); if (EntityManager.HasComponent(data.User)) { var kick = Vector2.One * data.Strength; - _sharedCameraRecoil.KickCamera(data.User, kick); + _cameraRecoil.KickCamera(data.User, kick); } _throwingSystem.TryThrow(ent, data.Direction, data.Strength, data.User, GetPushbackRatioFromPower(comp.Power)); @@ -250,7 +252,7 @@ namespace Content.Server.PneumaticCannon // we checked for this earlier in HasGas so a GetComp is okay var gas = EntityManager.GetComponent(contained); var environment = _atmos.GetContainingMixture(comp.Owner, false, true); - var removed = gas.RemoveAir(GetMoleUsageFromPower(comp.Power)); + var removed = _gasTank.RemoveAir(gas, GetMoleUsageFromPower(comp.Power)); if (environment != null && removed != null) { _atmos.Merge(environment, removed); diff --git a/Content.Shared/Alert/AlertCategory.cs b/Content.Shared/Alert/AlertCategory.cs index df310ca469..e6e0845385 100644 --- a/Content.Shared/Alert/AlertCategory.cs +++ b/Content.Shared/Alert/AlertCategory.cs @@ -10,6 +10,7 @@ public enum AlertCategory Breathing, Buckled, Health, + Internals, Stamina, Piloting, Hunger, diff --git a/Content.Shared/Alert/AlertType.cs b/Content.Shared/Alert/AlertType.cs index 208ec62f3e..5adfa594b1 100644 --- a/Content.Shared/Alert/AlertType.cs +++ b/Content.Shared/Alert/AlertType.cs @@ -30,6 +30,7 @@ Pulled, Pulling, Magboots, + Internals, Toxins, Muted, VowOfSilence, diff --git a/Content.Shared/Movement/Systems/SharedJetpackSystem.cs b/Content.Shared/Movement/Systems/SharedJetpackSystem.cs index 8d8cdb3fa5..7a1c9e19f9 100644 --- a/Content.Shared/Movement/Systems/SharedJetpackSystem.cs +++ b/Content.Shared/Movement/Systems/SharedJetpackSystem.cs @@ -1,7 +1,6 @@ using Content.Shared.Actions; using Content.Shared.Gravity; using Content.Shared.Interaction.Events; -using Content.Shared.Inventory; using Content.Shared.Movement.Components; using Content.Shared.Movement.Events; using Content.Shared.Popups; @@ -17,10 +16,11 @@ namespace Content.Shared.Movement.Systems; public abstract class SharedJetpackSystem : EntitySystem { + [Dependency] protected readonly IMapManager MapManager = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly INetManager _network = default!; [Dependency] protected readonly SharedContainerSystem Container = default!; - [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; + [Dependency] protected readonly MovementSpeedModifierSystem MovementSpeedModifier = default!; [Dependency] private readonly SharedPopupSystem _popups = default!; public override void Initialize() @@ -178,7 +178,7 @@ public abstract class SharedJetpackSystem : EntitySystem RemoveUser(user.Value); } - _movementSpeedModifier.RefreshMovementSpeedModifiers(user.Value); + MovementSpeedModifier.RefreshMovementSpeedModifiers(user.Value); } TryComp(component.Owner, out var appearance); diff --git a/Resources/Locale/en-US/actions/actions/internals.ftl b/Resources/Locale/en-US/actions/actions/internals.ftl index cc24d80037..ead73cf1e5 100644 --- a/Resources/Locale/en-US/actions/actions/internals.ftl +++ b/Resources/Locale/en-US/actions/actions/internals.ftl @@ -1,2 +1,5 @@ action-name-internals-toggle = Toggle Internals -action-description-internals-toggle = Breathe from the equipped gas tank. Also requires equipped breath mask. \ No newline at end of file +action-description-internals-toggle = Breathe from the equipped gas tank. Also requires equipped breath mask. + +internals-no-breath-tool = You are not wearing a breathing tool +internals-no-tank = You are not wearing a gas tank diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index 3132191550..e2c53944ad 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -5,6 +5,8 @@ id: BaseAlertOrder order: - category: Health + - category: Stamina + - category: Internals - alertType: Fire - alertType: Handcuffed - category: Buckled @@ -142,6 +144,18 @@ minSeverity: 0 maxSeverity: 6 +- type: alert + id: Internals + category: Internals + onClick: !type:ToggleInternals {} + icon: + sprite: /Textures/Interface/Alerts/internals.rsi + state: internal + name: Toggle internals + description: "Toggles your gas tank internals on or off." + minSeverity: 0 + maxSeverity: 2 + - type: alert id: PilotingShuttle category: Piloting diff --git a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml index 0b1c3ffe9d..d934756a06 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml @@ -20,9 +20,14 @@ toggleAction: name: action-name-internals-toggle description: action-description-internals-toggle - icon: Interface/Actions/internal0.png - iconOn: Interface/Actions/internal1.png + icon: + sprite: Interface/Alerts/internals.rsi + state: internal2 + iconOn: + sprite: Interface/Alerts/internals.rsi + state: internal1 event: !type:ToggleActionEvent + useDelay: 1.0 - type: Explosive explosionType: Default maxIntensity: 20 diff --git a/Resources/Textures/Interface/Actions/internal0.png b/Resources/Textures/Interface/Actions/internal0.png deleted file mode 100644 index c136016c64..0000000000 Binary files a/Resources/Textures/Interface/Actions/internal0.png and /dev/null differ diff --git a/Resources/Textures/Interface/Actions/internal1.png b/Resources/Textures/Interface/Actions/internal1.png deleted file mode 100644 index f405a3af01..0000000000 Binary files a/Resources/Textures/Interface/Actions/internal1.png and /dev/null differ diff --git a/Resources/Textures/Interface/Alerts/internals.rsi/internal0.png b/Resources/Textures/Interface/Alerts/internals.rsi/internal0.png new file mode 100644 index 0000000000..95c4644ec0 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/internals.rsi/internal0.png differ diff --git a/Resources/Textures/Interface/Alerts/internals.rsi/internal1.png b/Resources/Textures/Interface/Alerts/internals.rsi/internal1.png new file mode 100644 index 0000000000..7ee4c9de7c Binary files /dev/null and b/Resources/Textures/Interface/Alerts/internals.rsi/internal1.png differ diff --git a/Resources/Textures/Interface/Alerts/internals.rsi/internal2.png b/Resources/Textures/Interface/Alerts/internals.rsi/internal2.png new file mode 100644 index 0000000000..401c72e3c0 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/internals.rsi/internal2.png differ diff --git a/Resources/Textures/Interface/Alerts/internals.rsi/meta.json b/Resources/Textures/Interface/Alerts/internals.rsi/meta.json new file mode 100644 index 0000000000..9ce9b2330d --- /dev/null +++ b/Resources/Textures/Interface/Alerts/internals.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/tgstation/tgstation/blob/c7e930edde846fa51812ff8aed66fa97821ac000/icons/hud/screen_gen.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "internal0", + "delays": [ + [ + 0.5, + 0.5 + ] + ] + }, + { + "name": "internal1" + }, + { + "name": "internal2" + } + ] +} \ No newline at end of file