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;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IUserInterfaceManager _ui = default!;
public event EventHandler? ClearAlerts;
public event EventHandler<IReadOnlyDictionary<AlertKey, AlertState>>? SyncAlerts;
SubscribeLocalEvent<AlertsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<AlertsComponent, ComponentHandleState>(OnHandleState);
}
+
+ protected override void HandledAlert()
+ {
+ _ui.ClickSound();
+ }
+
protected override void LoadPrototypes()
{
base.LoadPrototypes();
--- /dev/null
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.EntitySystems;
+
+namespace Content.Client.Atmos.EntitySystems;
+
+public sealed class GasTankSystem : SharedGasTankSystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<GasTankComponent, AfterAutoHandleStateEvent>(OnGasTankState);
+ }
+
+ private void OnGasTankState(Entity<GasTankComponent> ent, ref AfterAutoHandleStateEvent args)
+ {
+ if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui))
+ {
+ bui.Update<GasTankBoundUserInterfaceState>();
+ }
+ }
+
+ public override void UpdateUserInterface(Entity<GasTankComponent> ent)
+ {
+ if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui))
+ {
+ bui.Update<GasTankBoundUserInterfaceState>();
+ }
+ }
+}
--- /dev/null
+using Content.Client.Atmos.UI;
+using Content.Shared.Atmos.Piping.Binary.Components;
+using Content.Shared.Atmos.Piping.Unary.Components;
+using Content.Shared.Atmos.Piping.Unary.Systems;
+using Content.Shared.NodeContainer;
+
+namespace Content.Client.Atmos.Piping.Unary.Systems;
+
+public sealed class GasCanisterSystem : SharedGasCanisterSystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<GasCanisterComponent, AfterAutoHandleStateEvent>(OnGasState);
+ }
+
+ private void OnGasState(Entity<GasCanisterComponent> ent, ref AfterAutoHandleStateEvent args)
+ {
+ if (UI.TryGetOpenUi<GasCanisterBoundUserInterface>(ent.Owner, GasCanisterUiKey.Key, out var bui))
+ {
+ bui.Update<GasCanisterBoundUserInterfaceState>();
+ }
+ }
+
+ protected override void DirtyUI(EntityUid uid, GasCanisterComponent? component = null, NodeContainerComponent? nodes = null)
+ {
+ if (UI.TryGetOpenUi<GasCanisterBoundUserInterface>(uid, GasCanisterUiKey.Key, out var bui))
+ {
+ bui.Update<GasCanisterBoundUserInterfaceState>();
+ }
+ }
+}
-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;
private void OnTankEjectPressed()
{
- SendMessage(new GasCanisterHoldingTankEjectMessage());
+ SendPredictedMessage(new GasCanisterHoldingTankEjectMessage());
}
private void OnReleasePressureSet(float value)
{
- SendMessage(new GasCanisterChangeReleasePressureMessage(value));
+ SendPredictedMessage(new GasCanisterChangeReleasePressureMessage(value));
}
private void OnReleaseValveOpenPressed()
{
- SendMessage(new GasCanisterChangeReleaseValveMessage(true));
+ SendPredictedMessage(new GasCanisterChangeReleaseValveMessage(true));
}
private void OnReleaseValveClosePressed()
{
- SendMessage(new GasCanisterChangeReleaseValveMessage(false));
+ SendPredictedMessage(new GasCanisterChangeReleaseValveMessage(false));
}
/// <summary>
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)
[UsedImplicitly]
public sealed class GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
- [ViewVariables]
- private const float MaxPressure = Atmospherics.MaxOutputPressure;
-
[ViewVariables]
private GasPressurePumpWindow? _window;
--- /dev/null
+using Content.Shared.Atmos.Components;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Systems;
+
+namespace Content.Client.Body.Systems;
+
+public sealed class InternalsSystem : SharedInternalsSystem
+{
+ [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<InternalsComponent, AfterAutoHandleStateEvent>(OnInternalsAfterState);
+ }
+
+ private void OnInternalsAfterState(Entity<InternalsComponent> ent, ref AfterAutoHandleStateEvent args)
+ {
+ if (ent.Comp.GasTankEntity != null && _ui.TryGetOpenUi(ent.Comp.GasTankEntity.Value, SharedGasTankUiKey.Key, out var bui))
+ {
+ bui.Update();
+ }
+ }
+}
{
public sealed class AlertControl : BaseButton
{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+
public AlertPrototype Alert { get; }
/// <summary>
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;
/// <param name="severity">severity of alert, null if alert doesn't have severity levels</param>
public AlertControl(AlertPrototype alert, short? severity)
{
- _gameTiming = IoCManager.Resolve<IGameTiming>();
- _entityManager = IoCManager.Resolve<IEntityManager>();
+ // Alerts will handle this.
+ MuteSounds = true;
+
+ IoCManager.InjectDependencies(this);
TooltipSupplier = SupplyTooltip;
Alert = alert;
_severity = severity;
using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.EntitySystems;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
public void SetOutputPressure(float value)
{
- SendMessage(new GasTankSetPressureMessage
+ SendPredictedMessage(new GasTankSetPressureMessage
{
Pressure = value
});
public void ToggleInternals()
{
- SendMessage(new GasTankToggleInternalsMessage());
+ SendPredictedMessage(new GasTankToggleInternalsMessage());
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<GasTankWindow>();
+ _window.Entity = Owner;
_window.SetTitle(EntMan.GetComponent<MetaDataComponent>(Owner).EntityName);
_window.OnOutputPressure += SetOutputPressure;
_window.OnToggleInternals += ToggleInternals;
{
base.UpdateState(state);
+ if (EntMan.TryGetComponent(Owner, out GasTankComponent? component))
+ {
+ var canConnect = EntMan.System<SharedGasTankSystem>().CanConnectToInternals((Owner, component));
+ _window?.Update(canConnect, component.IsConnected, component.OutputPressure);
+ }
+
if (state is GasTankBoundUserInterfaceState cast)
_window?.UpdateState(cast);
}
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;
public sealed class GasTankWindow
: BaseWindow
{
+ [Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IResourceCache _cache = default!;
private readonly RichTextLabel _lblPressure;
private readonly Button _btnInternals;
private readonly Label _topLabel;
+ public EntityUid Entity;
+
public event Action<float>? OnOutputPressure;
public event Action? OnToggleInternals;
public void UpdateState(GasTankBoundUserInterfaceState state)
{
_lblPressure.SetMarkup(Loc.GetString("gas-tank-window-tank-pressure-text", ("tankPressure", $"{state.TankPressure:0.##}")));
- _btnInternals.Disabled = !state.CanConnectInternals;
+ }
+
+ public void Update(bool canConnectInternals, bool internalsConnected, float outputPressure)
+ {
+ _btnInternals.Disabled = !canConnectInternals;
_lblInternals.SetMarkup(Loc.GetString("gas-tank-window-internal-text",
- ("status", Loc.GetString(state.InternalsConnected ? "gas-tank-window-internal-connected" : "gas-tank-window-internal-disconnected"))));
- if (state.OutputPressure.HasValue)
+ ("status", Loc.GetString(internalsConnected ? "gas-tank-window-internal-connected" : "gas-tank-window-internal-disconnected"))));
+ _spbPressure.Value = outputPressure;
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ // Easier than managing state on any ent changes. Previously this was just ticked on server's GasTankSystem.
+ if (_entManager.TryGetComponent(Entity, out GasTankComponent? tank))
+ {
+ var canConnectInternals = _entManager.System<SharedGasTankSystem>().CanConnectToInternals((Entity, tank));
+ _btnInternals.Disabled = !canConnectInternals;
+ }
+
+ if (!_btnInternals.Disabled)
{
- _spbPressure.Value = state.OutputPressure.Value;
+ _btnInternals.Disabled = _entManager.System<UseDelaySystem>().IsDelayed(Entity, id: SharedGasTankSystem.GasTankDelay);
}
}
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;
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;
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;
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;
+++ /dev/null
-using Content.Shared.Inventory;
-
-namespace Content.Server.Atmos.Components
-{
- /// <summary>
- /// Used in internals as breath tool.
- /// </summary>
- [RegisterComponent]
- [ComponentProtoName("BreathMask")]
- public sealed partial class BreathToolComponent : Component
- {
- /// <summary>
- /// Tool is functional only in allowed slots
- /// </summary>
- [DataField]
- public SlotFlags AllowedSlots = SlotFlags.MASK | SlotFlags.HEAD;
- public bool IsFunctional;
-
- public EntityUid? ConnectedInternalsEntity;
- }
-}
+++ /dev/null
-using Content.Shared.Atmos;
-using Robust.Shared.Audio;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Atmos.Components
-{
- [RegisterComponent]
- public sealed partial class GasTankComponent : Component, IGasMixtureHolder
- {
- public const float MaxExplosionRange = 26f;
- private const float DefaultLowPressure = 0f;
- private const float DefaultOutputPressure = Atmospherics.OneAtmosphere;
-
- public int Integrity = 3;
- public bool IsLowPressure => (Air?.Pressure ?? 0F) <= TankLowPressure;
-
- [ViewVariables(VVAccess.ReadWrite), DataField("ruptureSound")]
- public SoundSpecifier RuptureSound = new SoundPathSpecifier("/Audio/Effects/spray.ogg");
-
- [ViewVariables(VVAccess.ReadWrite), DataField("connectSound")]
- public SoundSpecifier? ConnectSound =
- new SoundPathSpecifier("/Audio/Effects/internals.ogg")
- {
- Params = AudioParams.Default.WithVolume(5f),
- };
-
- [ViewVariables(VVAccess.ReadWrite), DataField("disconnectSound")]
- public SoundSpecifier? DisconnectSound;
-
- // Cancel toggles sounds if we re-toggle again.
-
- public EntityUid? ConnectStream;
- public EntityUid? DisconnectStream;
-
- [DataField("air"), ViewVariables(VVAccess.ReadWrite)]
- public GasMixture Air { get; set; } = new();
-
- /// <summary>
- /// Pressure at which tank should be considered 'low' such as for internals.
- /// </summary>
- [DataField("tankLowPressure"), ViewVariables(VVAccess.ReadWrite)]
- public float TankLowPressure = DefaultLowPressure;
-
- /// <summary>
- /// Distributed pressure.
- /// </summary>
- [DataField("outputPressure"), ViewVariables(VVAccess.ReadWrite)]
- public float OutputPressure = DefaultOutputPressure;
-
- /// <summary>
- /// The maximum allowed output pressure.
- /// </summary>
- [DataField("maxOutputPressure"), ViewVariables(VVAccess.ReadWrite)]
- public float MaxOutputPressure = 3 * DefaultOutputPressure;
-
- /// <summary>
- /// Tank is connected to internals.
- /// </summary>
- [ViewVariables]
- public bool IsConnected => User != null;
-
- [ViewVariables]
- public EntityUid? User;
-
- /// <summary>
- /// True if this entity was recently moved out of a container. This might have been a hand -> inventory
- /// transfer, or it might have been the user dropping the tank. This indicates the tank needs to be checked.
- /// </summary>
- [ViewVariables]
- public bool CheckUser;
-
- /// <summary>
- /// Pressure at which tanks start leaking.
- /// </summary>
- [DataField("tankLeakPressure"), ViewVariables(VVAccess.ReadWrite)]
- public float TankLeakPressure = 30 * Atmospherics.OneAtmosphere;
-
- /// <summary>
- /// Pressure at which tank spills all contents into atmosphere.
- /// </summary>
- [DataField("tankRupturePressure"), ViewVariables(VVAccess.ReadWrite)]
- public float TankRupturePressure = 40 * Atmospherics.OneAtmosphere;
-
- /// <summary>
- /// Base 3x3 explosion.
- /// </summary>
- [DataField("tankFragmentPressure"), ViewVariables(VVAccess.ReadWrite)]
- public float TankFragmentPressure = 50 * Atmospherics.OneAtmosphere;
-
- /// <summary>
- /// Increases explosion for each scale kPa above threshold.
- /// </summary>
- [DataField("tankFragmentScale"), ViewVariables(VVAccess.ReadWrite)]
- public float TankFragmentScale = 2 * Atmospherics.OneAtmosphere;
-
- [DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
- public string ToggleAction = "ActionToggleInternals";
-
- [DataField("toggleActionEntity")] public EntityUid? ToggleActionEntity;
-
- /// <summary>
- /// Valve to release gas from tank
- /// </summary>
- [DataField("isValveOpen"), ViewVariables(VVAccess.ReadWrite)]
- public bool IsValveOpen = false;
-
- /// <summary>
- /// Gas release rate in L/s
- /// </summary>
- [DataField("valveOutputRate"), ViewVariables(VVAccess.ReadWrite)]
- public float ValveOutputRate = 100f;
-
- [DataField("valveSound"), ViewVariables(VVAccess.ReadWrite)]
- public SoundSpecifier ValveSound =
- new SoundCollectionSpecifier("valveSqueak")
- {
- Params = AudioParams.Default.WithVolume(-5f),
- };
- }
-}
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.DeviceNetwork.Components;
+using Content.Shared.NodeContainer;
namespace Content.Server.Atmos.Consoles;
+++ /dev/null
-using Content.Server.Atmos.Components;
-using Content.Server.Body.Components;
-
-namespace Content.Server.Atmos.EntitySystems;
-
-public sealed partial class AtmosphereSystem
-{
- private void InitializeBreathTool()
- {
- SubscribeLocalEvent<BreathToolComponent, ComponentShutdown>(OnBreathToolShutdown);
- }
-
- private void OnBreathToolShutdown(Entity<BreathToolComponent> entity, ref ComponentShutdown args)
- {
- DisconnectInternals(entity);
- }
-
- public void DisconnectInternals(Entity<BreathToolComponent> entity)
- {
- var old = entity.Comp.ConnectedInternalsEntity;
- entity.Comp.ConnectedInternalsEntity = null;
-
- if (TryComp<InternalsComponent>(old, out var internalsComponent))
- {
- _internals.DisconnectBreathTool((old.Value, internalsComponent), entity.Owner);
- }
-
- entity.Comp.IsFunctional = false;
- }
-}
private void OnGridAtmosphereInit(EntityUid uid, GridAtmosphereComponent component, ComponentInit args)
{
- base.Initialize();
-
EnsureComp<GasTileOverlayComponent>(uid);
foreach (var tile in component.Tiles.Values)
{
[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!;
UpdatesAfter.Add(typeof(NodeGroupSystem));
- InitializeBreathTool();
InitializeGases();
InitializeCommands();
InitializeCVars();
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;
-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;
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!;
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent<GasTankComponent, ComponentShutdown>(OnGasShutdown);
- SubscribeLocalEvent<GasTankComponent, BeforeActivatableUIOpenEvent>(BeforeUiOpen);
- SubscribeLocalEvent<GasTankComponent, GetItemActionsEvent>(OnGetActions);
- SubscribeLocalEvent<GasTankComponent, ExaminedEvent>(OnExamined);
- SubscribeLocalEvent<GasTankComponent, ToggleActionEvent>(OnActionToggle);
SubscribeLocalEvent<GasTankComponent, EntParentChangedMessage>(OnParentChange);
- SubscribeLocalEvent<GasTankComponent, GasTankSetPressureMessage>(OnGasTankSetPressure);
- SubscribeLocalEvent<GasTankComponent, GasTankToggleInternalsMessage>(OnGasTankToggleInternals);
SubscribeLocalEvent<GasTankComponent, GasAnalyzerScanEvent>(OnAnalyzed);
SubscribeLocalEvent<GasTankComponent, PriceCalculationEvent>(OnGasTankPrice);
- SubscribeLocalEvent<GasTankComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerb);
Subs.CVar(_cfg, CCVars.AtmosTankFragment, UpdateMaxRange, true);
}
_maxExplosionRange = value;
}
- private void OnGasShutdown(Entity<GasTankComponent> gasTank, ref ComponentShutdown args)
- {
- DisconnectFromInternals(gasTank);
- }
-
- private void OnGasTankToggleInternals(Entity<GasTankComponent> ent, ref GasTankToggleInternalsMessage args)
- {
- ToggleInternals(ent);
- }
-
- private void OnGasTankSetPressure(Entity<GasTankComponent> ent, ref GasTankSetPressureMessage args)
- {
- var pressure = Math.Clamp(args.Pressure, 0f, ent.Comp.MaxOutputPressure);
-
- ent.Comp.OutputPressure = pressure;
-
- UpdateUserInterface(ent, true);
- }
-
- public void UpdateUserInterface(Entity<GasTankComponent> ent, bool initialUpdate = false)
+ public override void UpdateUserInterface(Entity<GasTankComponent> ent)
{
var (owner, component) = ent;
_ui.SetUiState(owner, SharedGasTankUiKey.Key,
new GasTankBoundUserInterfaceState
{
TankPressure = component.Air?.Pressure ?? 0,
- OutputPressure = initialUpdate ? component.OutputPressure : null,
- InternalsConnected = component.IsConnected,
- CanConnectInternals = CanConnectToInternals(ent)
});
}
- private void BeforeUiOpen(Entity<GasTankComponent> ent, ref BeforeActivatableUIOpenEvent args)
- {
- // Only initial update includes output pressure information, to avoid overwriting client-input as the updates come in.
- UpdateUserInterface(ent, true);
- }
-
private void OnParentChange(EntityUid uid, GasTankComponent component, ref EntParentChangedMessage args)
{
// When an item is moved from hands -> pockets, the container removal briefly dumps the item on the floor.
component.CheckUser = true;
}
- private void OnGetActions(EntityUid uid, GasTankComponent component, GetItemActionsEvent args)
- {
- args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
- }
-
- private void OnExamined(EntityUid uid, GasTankComponent component, ExaminedEvent args)
- {
- using var _ = args.PushGroup(nameof(GasTankComponent));
- if (args.IsInDetailsRange)
- args.PushMarkup(Loc.GetString("comp-gas-tank-examine", ("pressure", Math.Round(component.Air?.Pressure ?? 0))));
- if (component.IsConnected)
- args.PushMarkup(Loc.GetString("comp-gas-tank-connected"));
- args.PushMarkup(Loc.GetString(component.IsValveOpen ? "comp-gas-tank-examine-open-valve" : "comp-gas-tank-examine-closed-valve"));
- }
-
- private void OnActionToggle(Entity<GasTankComponent> gasTank, ref ToggleActionEvent args)
- {
- if (args.Handled)
- return;
-
- ToggleInternals(gasTank);
- args.Handled = true;
- }
-
public override void Update(float frameTime)
{
base.Update(frameTime);
{
_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);
}
_audioSys.PlayPvs(gasTank.Comp.RuptureSound, gasTank);
}
- private void ToggleInternals(Entity<GasTankComponent> ent)
- {
- if (ent.Comp.IsConnected)
- {
- DisconnectFromInternals(ent);
- }
- else
- {
- ConnectToInternals(ent);
- }
- }
-
public GasMixture? RemoveAir(Entity<GasTankComponent> gasTank, float amount)
{
var gas = gasTank.Comp.Air?.Remove(amount);
return air;
}
- public bool CanConnectToInternals(Entity<GasTankComponent> ent)
- {
- TryGetInternalsComp(ent, out _, out var internalsComp, ent.Comp.User);
- return internalsComp != null && internalsComp.BreathTools.Count != 0 && !ent.Comp.IsValveOpen;
- }
-
- public void ConnectToInternals(Entity<GasTankComponent> ent)
- {
- var (owner, component) = ent;
- if (component.IsConnected || !CanConnectToInternals(ent))
- return;
-
- TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, ent.Comp.User);
- if (internalsUid == null || internalsComp == null)
- return;
-
- if (_internals.TryConnectTank((internalsUid.Value, internalsComp), owner))
- component.User = internalsUid.Value;
-
- _actions.SetToggled(component.ToggleActionEntity, component.IsConnected);
-
- // Couldn't toggle!
- if (!component.IsConnected)
- return;
-
- component.ConnectStream = _audioSys.Stop(component.ConnectStream);
- component.ConnectStream = _audioSys.PlayPvs(component.ConnectSound, owner)?.Entity;
-
- UpdateUserInterface(ent);
- }
-
- public void DisconnectFromInternals(Entity<GasTankComponent> ent)
- {
- var (owner, component) = ent;
-
- if (component.User == null)
- return;
-
- TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, component.User);
- component.User = null;
-
- _actions.SetToggled(component.ToggleActionEntity, false);
-
- if (internalsUid != null && internalsComp != null)
- _internals.DisconnectTank((internalsUid.Value, internalsComp));
- component.DisconnectStream = _audioSys.Stop(component.DisconnectStream);
- component.DisconnectStream = _audioSys.PlayPvs(component.DisconnectSound, owner)?.Entity;
-
- UpdateUserInterface(ent);
- }
-
- /// <summary>
- /// Tries to retrieve the internals component of either the gas tank's user,
- /// or the gas tank's... containing container
- /// </summary>
- /// <param name="user">The user of the gas tank</param>
- /// <returns>True if internals comp isn't null, false if it is null</returns>
- private bool TryGetInternalsComp(Entity<GasTankComponent> ent, out EntityUid? internalsUid, out InternalsComponent? internalsComp, EntityUid? user = null)
- {
- internalsUid = default;
- internalsComp = default;
-
- // If the gas tank doesn't exist for whatever reason, don't even bother
- if (TerminatingOrDeleted(ent.Owner))
- return false;
-
- user ??= ent.Comp.User;
- // Check if the gas tank's user actually has the component that allows them to use a gas tank and mask
- if (TryComp<InternalsComponent>(user, out var userInternalsComp) && userInternalsComp != null)
- {
- internalsUid = user;
- internalsComp = userInternalsComp;
- return true;
- }
-
- // Yeah I have no clue what this actually does, I appreciate the lack of comments on the original function
- if (_containers.TryGetContainingContainer((ent.Owner, Transform(ent.Owner)), out var container) && container != null)
- {
- if (TryComp<InternalsComponent>(container.Owner, out var containerInternalsComp) && containerInternalsComp != null)
- {
- internalsUid = container.Owner;
- internalsComp = containerInternalsComp;
- return true;
- }
- }
-
- return false;
- }
-
public void AssumeAir(Entity<GasTankComponent> ent, GasMixture giver)
{
_atmosphereSystem.Merge(ent.Comp.Air, giver);
{
args.Price += _atmosphereSystem.GetPrice(component.Air);
}
-
- private void OnGetAlternativeVerb(EntityUid uid, GasTankComponent component, GetVerbsEvent<AlternativeVerb> args)
- {
- if (!args.CanAccess || !args.CanInteract || args.Hands == null)
- return;
- args.Verbs.Add(new AlternativeVerb()
- {
- Text = component.IsValveOpen ? Loc.GetString("comp-gas-tank-close-valve") : Loc.GetString("comp-gas-tank-open-valve"),
- Act = () =>
- {
- component.IsValveOpen = !component.IsValveOpen;
- _audioSys.PlayPvs(component.ValveSound, uid);
- },
- Disabled = component.IsConnected,
- });
- }
}
}
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;
+++ /dev/null
-using Content.Shared.Atmos;
-
-namespace Content.Server.Atmos
-{
- public interface IGasMixtureHolder
- {
- public GasMixture Air { get; set; }
- }
-}
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;
using Content.Shared.Atmos;
using Content.Shared.Construction.Components;
using Content.Shared.Destructible;
+using Content.Shared.NodeContainer;
using Content.Shared.Popups;
using JetBrains.Annotations;
+++ /dev/null
-using Content.Shared.Atmos;
-using Content.Shared.Containers.ItemSlots;
-using Content.Shared.Guidebook;
-using Robust.Shared.Audio;
-
-namespace Content.Server.Atmos.Piping.Unary.Components
-{
- [RegisterComponent]
- public sealed partial class GasCanisterComponent : Component, IGasMixtureHolder
- {
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("port")]
- public string PortName { get; set; } = "port";
-
- /// <summary>
- /// Container name for the gas tank holder.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("container")]
- public string ContainerName { get; set; } = "tank_slot";
-
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField]
- public ItemSlot GasTankSlot = new();
-
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("gasMixture")]
- public GasMixture Air { get; set; } = new();
-
- /// <summary>
- /// Last recorded pressure, for appearance-updating purposes.
- /// </summary>
- public float LastPressure { get; set; } = 0f;
-
- /// <summary>
- /// Minimum release pressure possible for the release valve.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("minReleasePressure")]
- public float MinReleasePressure { get; set; } = Atmospherics.OneAtmosphere / 10;
-
- /// <summary>
- /// Maximum release pressure possible for the release valve.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("maxReleasePressure")]
- public float MaxReleasePressure { get; set; } = Atmospherics.OneAtmosphere * 10;
-
- /// <summary>
- /// Valve release pressure.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("releasePressure")]
- public float ReleasePressure { get; set; } = Atmospherics.OneAtmosphere;
-
- /// <summary>
- /// Whether the release valve is open on the canister.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("releaseValve")]
- public bool ReleaseValve { get; set; } = false;
-
- [DataField("accessDeniedSound")]
- public SoundSpecifier AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
-
- #region GuidebookData
-
- [GuidebookData]
- public float Volume => Air.Volume;
-
- #endregion
- }
-}
-using Content.Server.Administration.Logs;
-using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
-using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.Cargo.Systems;
-using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
-using Content.Server.Popups;
using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
-using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Atmos.Piping.Unary.Systems;
using Content.Shared.Database;
-using Content.Shared.Interaction;
-using Content.Shared.Lock;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Containers;
-using Robust.Shared.Player;
+using Content.Shared.NodeContainer;
+using GasCanisterComponent = Content.Shared.Atmos.Piping.Unary.Components.GasCanisterComponent;
namespace Content.Server.Atmos.Piping.Unary.EntitySystems;
-public sealed class GasCanisterSystem : EntitySystem
+public sealed class GasCanisterSystem : SharedGasCanisterSystem
{
[Dependency] private readonly AtmosphereSystem _atmos = default!;
- [Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly PopupSystem _popup = default!;
- [Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
- [Dependency] private readonly ItemSlotsSystem _slots = default!;
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent<GasCanisterComponent, ComponentStartup>(OnCanisterStartup);
SubscribeLocalEvent<GasCanisterComponent, AtmosDeviceUpdateEvent>(OnCanisterUpdated);
- SubscribeLocalEvent<GasCanisterComponent, ActivateInWorldEvent>(OnCanisterActivate, after: new[] { typeof(LockSystem) });
- SubscribeLocalEvent<GasCanisterComponent, InteractHandEvent>(OnCanisterInteractHand);
- SubscribeLocalEvent<GasCanisterComponent, ItemSlotInsertAttemptEvent>(OnCanisterInsertAttempt);
- SubscribeLocalEvent<GasCanisterComponent, EntInsertedIntoContainerMessage>(OnCanisterContainerInserted);
- SubscribeLocalEvent<GasCanisterComponent, EntRemovedFromContainerMessage>(OnCanisterContainerRemoved);
SubscribeLocalEvent<GasCanisterComponent, PriceCalculationEvent>(CalculateCanisterPrice);
SubscribeLocalEvent<GasCanisterComponent, GasAnalyzerScanEvent>(OnAnalyzed);
- // Bound UI subscriptions
- SubscribeLocalEvent<GasCanisterComponent, GasCanisterHoldingTankEjectMessage>(OnHoldingTankEjectMessage);
- SubscribeLocalEvent<GasCanisterComponent, GasCanisterChangeReleasePressureMessage>(OnCanisterChangeReleasePressure);
- SubscribeLocalEvent<GasCanisterComponent, GasCanisterChangeReleaseValveMessage>(OnCanisterChangeReleaseValve);
}
/// <summary>
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)
{
var tank = canister.GasTankSlot.Item.Value;
var tankComponent = Comp<GasTankComponent>(tank);
- tankLabel = Name(tank);
tankPressure = tankComponent.Air.Pressure;
}
- _ui.SetUiState(uid, GasCanisterUiKey.Key,
- new GasCanisterBoundUserInterfaceState(Name(uid),
- canister.Air.Pressure, portStatus, tankLabel, tankPressure, canister.ReleasePressure,
- canister.ReleaseValve, canister.MinReleasePressure, canister.MaxReleasePressure));
- }
-
- private void OnHoldingTankEjectMessage(EntityUid uid, GasCanisterComponent canister, GasCanisterHoldingTankEjectMessage args)
- {
- if (canister.GasTankSlot.Item == null)
- return;
-
- var item = canister.GasTankSlot.Item;
- _slots.TryEjectToHands(uid, canister.GasTankSlot, args.Actor);
-
- if (canister.ReleaseValve)
- {
- _adminLogger.Add(LogType.CanisterTankEjected, LogImpact.High, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister} while the valve was open, releasing [{GetContainedGasesString((uid, canister))}] to atmosphere");
- }
- else
- {
- _adminLogger.Add(LogType.CanisterTankEjected, LogImpact.Medium, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister}");
- }
- }
-
- private void OnCanisterChangeReleasePressure(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleasePressureMessage args)
- {
- var pressure = Math.Clamp(args.Pressure, canister.MinReleasePressure, canister.MaxReleasePressure);
-
- _adminLogger.Add(LogType.CanisterPressure, LogImpact.Medium, $"{ToPrettyString(args.Actor):player} set the release pressure on {ToPrettyString(uid):canister} to {args.Pressure}");
-
- canister.ReleasePressure = pressure;
- DirtyUI(uid, canister);
- }
-
- private void OnCanisterChangeReleaseValve(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleaseValveMessage args)
- {
- // filling a jetpack with plasma is less important than filling a room with it
- var hasItem = canister.GasTankSlot.HasItem;
- var impact = hasItem ? LogImpact.Medium : LogImpact.High;
-
- _adminLogger.Add(
- LogType.CanisterValve,
- impact,
- $"{ToPrettyString(args.Actor):player} {(args.Valve ? "opened" : "closed")} the valve on {ToPrettyString(uid):canister} to {(hasItem ? "inserted tank" : "environment")} while it contained [{GetContainedGasesString((uid, canister))}]");
-
- canister.ReleaseValve = args.Valve;
- DirtyUI(uid, canister);
- }
-
- private static string GetContainedGasesString(Entity<GasCanisterComponent> canister)
- {
- return string.Join(", ", canister.Comp.Air);
+ UI.SetUiState(uid, GasCanisterUiKey.Key,
+ new GasCanisterBoundUserInterfaceState(canister.Air.Pressure, portStatus, tankPressure));
}
private void OnCanisterUpdated(EntityUid uid, GasCanisterComponent canister, ref AtmosDeviceUpdateEvent args)
}
}
- private void OnCanisterActivate(EntityUid uid, GasCanisterComponent component, ActivateInWorldEvent args)
- {
- if (!args.Complex)
- return;
-
- if (!TryComp<ActorComponent>(args.User, out var actor))
- return;
-
- if (CheckLocked(uid, component, args.User))
- return;
-
- // Needs to be here so the locked check still happens if the canister
- // is locked and you don't have permissions
- if (args.Handled)
- return;
-
- _ui.OpenUi(uid, GasCanisterUiKey.Key, actor.PlayerSession);
- args.Handled = true;
- }
-
- private void OnCanisterInteractHand(EntityUid uid, GasCanisterComponent component, InteractHandEvent args)
- {
- if (!TryComp<ActorComponent>(args.User, out var actor))
- return;
-
- if (CheckLocked(uid, component, args.User))
- return;
-
- _ui.OpenUi(uid, GasCanisterUiKey.Key, actor.PlayerSession);
- args.Handled = true;
- }
-
- private void OnCanisterInsertAttempt(EntityUid uid, GasCanisterComponent component, ref ItemSlotInsertAttemptEvent args)
- {
- if (args.Slot.ID != component.ContainerName || args.User == null)
- return;
-
- if (!TryComp<GasTankComponent>(args.Item, out var gasTank) || gasTank.IsValveOpen)
- {
- args.Cancelled = true;
- return;
- }
-
- // Preventing inserting a tank since if its locked you cant remove it.
- if (!CheckLocked(uid, component, args.User.Value))
- return;
-
- args.Cancelled = true;
- }
-
- private void OnCanisterContainerInserted(EntityUid uid, GasCanisterComponent component, EntInsertedIntoContainerMessage args)
- {
- if (args.Container.ID != component.ContainerName)
- return;
-
- DirtyUI(uid, component);
-
- _appearance.SetData(uid, GasCanisterVisuals.TankInserted, true);
- }
-
- private void OnCanisterContainerRemoved(EntityUid uid, GasCanisterComponent component, EntRemovedFromContainerMessage args)
- {
- if (args.Container.ID != component.ContainerName)
- return;
-
- DirtyUI(uid, component);
-
- _appearance.SetData(uid, GasCanisterVisuals.TankInserted, false);
- }
-
/// <summary>
/// Mix air from a gas container into a pipe net.
/// Useful for anything that uses connector ports.
args.GasMixtures.Add((Name(tank), tankComponent.Air));
}
}
-
- /// <summary>
- /// Check if the canister is locked, playing its sound and popup if so.
- /// </summary>
- /// <returns>
- /// True if locked, false otherwise.
- /// </returns>
- private bool CheckLocked(EntityUid uid, GasCanisterComponent comp, EntityUid user)
- {
- if (TryComp<LockComponent>(uid, out var lockComp) && lockComp.Locked)
- {
- _popup.PopupEntity(Loc.GetString("gas-canister-popup-denied"), uid, user);
- _audio.PlayPvs(comp.AccessDeniedSound, uid);
-
- return true;
- }
-
- return false;
- }
}
+++ /dev/null
-using Content.Shared.Alert;
-using Robust.Shared.Prototypes;
-
-namespace Content.Server.Body.Components
-{
- /// <summary>
- /// Handles hooking up a mask (breathing tool) / gas tank together and allowing the Owner to breathe through it.
- /// </summary>
- [RegisterComponent]
- public sealed partial class InternalsComponent : Component
- {
- [ViewVariables]
- public EntityUid? GasTankEntity;
-
- [ViewVariables]
- public HashSet<EntityUid> BreathTools { get; set; } = new();
-
- /// <summary>
- /// Toggle Internals delay when the target is not you.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField]
- public TimeSpan Delay = TimeSpan.FromSeconds(3);
-
- [DataField]
- public ProtoId<AlertPrototype> InternalsAlert = "Internals";
- }
-
-}
-using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
-using Content.Server.Body.Components;
using Content.Server.Popups;
using Content.Shared.Alert;
using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Systems;
using Content.Shared.DoAfter;
-using Content.Shared.Hands.Components;
using Content.Shared.Internals;
using Content.Shared.Inventory;
using Content.Shared.Roles;
-using Content.Shared.Verbs;
-using Robust.Shared.Containers;
-using Robust.Shared.Utility;
namespace Content.Server.Body.Systems;
-public sealed class InternalsSystem : EntitySystem
+public sealed class InternalsSystem : SharedInternalsSystem
{
[Dependency] private readonly AlertsSystem _alerts = default!;
- [Dependency] private readonly AtmosphereSystem _atmos = default!;
- [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly GasTankSystem _gasTank = default!;
- [Dependency] private readonly InventorySystem _inventory = default!;
- [Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly RespiratorSystem _respirator = default!;
private EntityQuery<InternalsComponent> _internalsQuery;
_internalsQuery = GetEntityQuery<InternalsComponent>();
SubscribeLocalEvent<InternalsComponent, InhaleLocationEvent>(OnInhaleLocation);
- SubscribeLocalEvent<InternalsComponent, ComponentStartup>(OnInternalsStartup);
- SubscribeLocalEvent<InternalsComponent, ComponentShutdown>(OnInternalsShutdown);
- SubscribeLocalEvent<InternalsComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs);
- SubscribeLocalEvent<InternalsComponent, InternalsDoAfterEvent>(OnDoAfter);
- SubscribeLocalEvent<InternalsComponent, ToggleInternalsAlertEvent>(OnToggleInternalsAlert);
-
SubscribeLocalEvent<InternalsComponent, StartingGearEquippedEvent>(OnStartingGear);
}
ToggleInternals(uid, uid, force: false, component);
}
- private void OnGetInteractionVerbs(
- Entity<InternalsComponent> ent,
- ref GetVerbsEvent<InteractionVerb> args)
- {
- if (!args.CanAccess || !args.CanInteract || args.Hands is null)
- return;
-
- if (!AreInternalsWorking(ent) && ent.Comp.BreathTools.Count == 0)
- return;
-
- var user = args.User;
-
- InteractionVerb verb = new()
- {
- Act = () =>
- {
- ToggleInternals(ent, user, force: false, ent);
- },
- Message = Loc.GetString("action-description-internals-toggle"),
- Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/dot.svg.192dpi.png")),
- Text = Loc.GetString("action-name-internals-toggle"),
- };
-
- args.Verbs.Add(verb);
- }
-
- public void ToggleInternals(
- EntityUid uid,
- EntityUid user,
- bool force,
- InternalsComponent? internals = null)
- {
- if (!Resolve(uid, ref internals, logMissing: false))
- return;
-
- // Toggle off if they're on
- if (AreInternalsWorking(internals))
- {
- if (force)
- {
- DisconnectTank((uid, internals));
- return;
- }
-
- StartToggleInternalsDoAfter(user, (uid, internals));
- return;
- }
-
- // If they're not on then check if we have a mask to use
- if (internals.BreathTools.Count == 0)
- {
- _popupSystem.PopupEntity(Loc.GetString("internals-no-breath-tool"), uid, user);
- return;
- }
-
- var tank = FindBestGasTank(uid);
-
- if (tank is null)
- {
- _popupSystem.PopupEntity(Loc.GetString("internals-no-tank"), uid, user);
- return;
- }
-
- if (!force)
- {
- StartToggleInternalsDoAfter(user, (uid, internals));
- return;
- }
-
- _gasTank.ConnectToInternals(tank.Value);
- }
-
- private void StartToggleInternalsDoAfter(EntityUid user, Entity<InternalsComponent> targetEnt)
- {
- // Is the target not you? If yes, use a do-after to give them time to respond.
- var isUser = user == targetEnt.Owner;
- var delay = !isUser ? targetEnt.Comp.Delay : TimeSpan.Zero;
-
- _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, delay, new InternalsDoAfterEvent(), targetEnt, target: targetEnt)
- {
- BreakOnDamage = true,
- BreakOnMove = true,
- MovementThreshold = 0.1f,
- });
- }
-
- private void OnDoAfter(Entity<InternalsComponent> ent, ref InternalsDoAfterEvent args)
- {
- if (args.Cancelled || args.Handled)
- return;
-
- ToggleInternals(ent, args.User, force: true, ent);
-
- args.Handled = true;
- }
-
- private void OnToggleInternalsAlert(Entity<InternalsComponent> ent, ref ToggleInternalsAlertEvent args)
- {
- if (args.Handled)
- return;
- ToggleInternals(ent, ent, false, internals: ent.Comp);
- args.Handled = true;
- }
-
- private void OnInternalsStartup(Entity<InternalsComponent> ent, ref ComponentStartup args)
- {
- _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
- }
-
- private void OnInternalsShutdown(Entity<InternalsComponent> ent, ref ComponentShutdown args)
- {
- _alerts.ClearAlert(ent, ent.Comp.InternalsAlert);
- }
-
private void OnInhaleLocation(Entity<InternalsComponent> ent, ref InhaleLocationEvent args)
{
if (AreInternalsWorking(ent))
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
}
}
- public void DisconnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
- {
- ent.Comp.BreathTools.Remove(toolEntity);
-
- if (TryComp(toolEntity, out BreathToolComponent? breathTool))
- _atmos.DisconnectInternals((toolEntity, breathTool));
-
- if (ent.Comp.BreathTools.Count == 0)
- DisconnectTank(ent);
-
- _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
- }
-
- public void ConnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
- {
- if (!ent.Comp.BreathTools.Add(toolEntity))
- return;
-
- _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
- }
-
- public void DisconnectTank(Entity<InternalsComponent> ent)
- {
- if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
- _gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank));
-
- ent.Comp.GasTankEntity = null;
- _alerts.ShowAlert(ent.Owner, ent.Comp.InternalsAlert, GetSeverity(ent.Comp));
- }
-
- public bool TryConnectTank(Entity<InternalsComponent> ent, EntityUid tankEntity)
- {
- if (ent.Comp.BreathTools.Count == 0)
- return false;
-
- if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
- _gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank));
-
- ent.Comp.GasTankEntity = tankEntity;
- _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
- return true;
- }
-
- public bool AreInternalsWorking(EntityUid uid, InternalsComponent? component = null)
- {
- return Resolve(uid, ref component, logMissing: false)
- && AreInternalsWorking(component);
- }
-
- public bool AreInternalsWorking(InternalsComponent component)
- {
- return TryComp(component.BreathTools.FirstOrNull(), out BreathToolComponent? breathTool)
- && breathTool.IsFunctional
- && HasComp<GasTankComponent>(component.GasTankEntity);
- }
-
- private short GetSeverity(InternalsComponent component)
- {
- if (component.BreathTools.Count == 0 || !AreInternalsWorking(component))
- return 2;
-
- // If pressure in the tank is below low pressure threshold, flash warning on internals UI
- if (TryComp<GasTankComponent>(component.GasTankEntity, out var gasTank)
- && gasTank.IsLowPressure)
- {
- return 0;
- }
-
- return 1;
- }
-
- public Entity<GasTankComponent>? FindBestGasTank(
- Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user)
- {
- // TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses
- // Prioritise
- // 1. back equipped tanks
- // 2. exo-slot tanks
- // 3. in-hand tanks
- // 4. pocket/belt tanks
-
- if (!Resolve(user, ref user.Comp2, ref user.Comp3))
- return null;
-
- if (_inventory.TryGetSlotEntity(user, "back", out var backEntity, user.Comp2, user.Comp3) &&
- TryComp<GasTankComponent>(backEntity, out var backGasTank) &&
- _gasTank.CanConnectToInternals((backEntity.Value, backGasTank)))
- {
- return (backEntity.Value, backGasTank);
- }
-
- if (_inventory.TryGetSlotEntity(user, "suitstorage", out var entity, user.Comp2, user.Comp3) &&
- TryComp<GasTankComponent>(entity, out var gasTank) &&
- _gasTank.CanConnectToInternals((entity.Value, gasTank)))
- {
- return (entity.Value, gasTank);
- }
-
- foreach (var item in _inventory.GetHandOrInventoryEntities((user.Owner, user.Comp1, user.Comp2)))
- {
- if (TryComp(item, out gasTank) && _gasTank.CanConnectToInternals((item, gasTank)))
- return (item, gasTank);
- }
-
- return null;
- }
}
-using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
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;
SubscribeLocalEvent<LungComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<BreathToolComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<BreathToolComponent, GotUnequippedEvent>(OnGotUnequipped);
- SubscribeLocalEvent<BreathToolComponent, ItemMaskToggledEvent>(OnMaskToggled);
}
private void OnGotUnequipped(Entity<BreathToolComponent> ent, ref GotUnequippedEvent args)
return;
}
- ent.Comp.IsFunctional = true;
-
if (TryComp(args.Equipee, out InternalsComponent? internals))
{
ent.Comp.ConnectedInternalsEntity = args.Equipee;
}
}
- private void OnMaskToggled(Entity<BreathToolComponent> ent, ref ItemMaskToggledEvent args)
- {
- if (args.Mask.Comp.IsToggled)
- {
- _atmos.DisconnectInternals(ent);
- }
- else
- {
- ent.Comp.IsFunctional = true;
-
- if (TryComp(args.Wearer, out InternalsComponent? internals))
- {
- ent.Comp.ConnectedInternalsEntity = args.Wearer;
- _internals.ConnectBreathTool((args.Wearer.Value, internals), ent);
- }
- }
- }
-
public void GasToReagent(EntityUid uid, LungComponent lung)
{
if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution))
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;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
namespace Content.Server.DeviceNetwork.Components
{
using Content.Server.Power.EntitySystems;
using Content.Server.Power.Nodes;
using Content.Shared.DeviceNetwork.Events;
+using Content.Shared.NodeContainer;
namespace Content.Server.DeviceNetwork.Systems
{
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
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;
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;
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
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;
+++ /dev/null
-using System.Diagnostics.CodeAnalysis;
-using Content.Server.NodeContainer.Nodes;
-
-namespace Content.Server.NodeContainer
-{
- /// <summary>
- /// Creates and maintains a set of <see cref="Node"/>s.
- /// </summary>
- [RegisterComponent]
- public sealed partial class NodeContainerComponent : Component
- {
- //HACK: THIS BEING readOnly IS A FILTHY HACK AND I HATE IT --moony
- [DataField("nodes", readOnly: true)] public Dictionary<string, Node> Nodes { get; private set; } = new();
-
- [DataField("examinable")] public bool Examinable = false;
- }
-}
using System.Linq;
using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.Server.NodeContainer.NodeGroups
{
- /// <summary>
- /// Maintains a collection of <see cref="Node"/>s, and performs operations requiring a list of
- /// all connected <see cref="Node"/>s.
- /// </summary>
- public interface INodeGroup
- {
- bool Remaking { get; }
-
- /// <summary>
- /// The list of nodes currently in this group.
- /// </summary>
- IReadOnlyList<Node> Nodes { get; }
-
- void Create(NodeGroupID groupId);
-
- void Initialize(Node sourceNode, IEntityManager entMan);
-
- void RemoveNode(Node node);
-
- void LoadNodes(List<Node> groupNodes);
-
- // In theory, the SS13 curse ensures this method will never be called.
- void AfterRemake(IEnumerable<IGrouping<INodeGroup?, Node>> newGroups);
-
- /// <summary>
- /// Return any additional data to display for the node-visualizer debug overlay.
- /// </summary>
- string? GetDebugData();
- }
-
[NodeGroup(NodeGroupID.Default, NodeGroupID.WireNet)]
[Virtual]
public class BaseNodeGroup : INodeGroup
+using Content.Shared.NodeContainer.NodeGroups;
using JetBrains.Annotations;
namespace Content.Server.NodeContainer.NodeGroups
using System.Reflection;
-using Content.Server.Power.Generation.Teg;
+using Content.Shared.NodeContainer.NodeGroups;
using Robust.Shared.Reflection;
namespace Content.Server.NodeContainer.NodeGroups
return instance;
}
}
-
- public enum NodeGroupID : byte
- {
- Default,
- HVPower,
- MVPower,
- Apc,
- AMEngine,
- Pipe,
- WireNet,
-
- /// <summary>
- /// Group used by the TEG.
- /// </summary>
- /// <seealso cref="TegSystem"/>
- /// <seealso cref="TegNodeGroup"/>
- Teg,
- }
}
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
+using Content.Shared.NodeContainer;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
+using Content.Shared.NodeContainer;
+
namespace Content.Server.NodeContainer.Nodes
{
/// <summary>
+++ /dev/null
-using Content.Server.NodeContainer.EntitySystems;
-using Content.Server.NodeContainer.NodeGroups;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-
-namespace Content.Server.NodeContainer.Nodes
-{
- /// <summary>
- /// Organizes themselves into distinct <see cref="INodeGroup"/>s with other <see cref="Node"/>s
- /// that they can "reach" and have the same <see cref="Node.NodeGroupID"/>.
- /// </summary>
- [ImplicitDataDefinitionForInheritors]
- public abstract partial class Node
- {
- /// <summary>
- /// An ID used as a criteria for combining into groups. Determines which <see cref="INodeGroup"/>
- /// implementation is used as a group, detailed in <see cref="INodeGroupFactory"/>.
- /// </summary>
- [DataField("nodeGroupID")]
- public NodeGroupID NodeGroupID { get; private set; } = NodeGroupID.Default;
-
- /// <summary>
- /// The node group this node is a part of.
- /// </summary>
- [ViewVariables] public INodeGroup? NodeGroup;
-
- /// <summary>
- /// The entity that owns this node via its <see cref="NodeContainerComponent"/>.
- /// </summary>
- [ViewVariables] public EntityUid Owner { get; private set; } = default!;
-
- /// <summary>
- /// If this node should be considered for connection by other nodes.
- /// </summary>
- public virtual bool Connectable(IEntityManager entMan, TransformComponent? xform = null)
- {
- if (Deleting)
- return false;
-
- if (entMan.IsQueuedForDeletion(Owner))
- return false;
-
- if (!NeedAnchored)
- return true;
-
- xform ??= entMan.GetComponent<TransformComponent>(Owner);
- return xform.Anchored;
- }
-
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("needAnchored")]
- public bool NeedAnchored { get; private set; } = true;
-
- public virtual void OnAnchorStateChanged(IEntityManager entityManager, bool anchored) { }
-
- /// <summary>
- /// Prevents a node from being used by other nodes while midway through removal.
- /// </summary>
- public bool Deleting;
-
- /// <summary>
- /// All compatible nodes that are reachable by this node.
- /// Effectively, active connections out of this node.
- /// </summary>
- public readonly HashSet<Node> ReachableNodes = new();
-
- internal int FloodGen;
- internal int UndirectGen;
- internal bool FlaggedForFlood;
- internal int NetId;
-
- /// <summary>
- /// Name of this node on the owning <see cref="NodeContainerComponent"/>.
- /// </summary>
- public string Name = default!;
-
- /// <summary>
- /// Invoked when the owning <see cref="NodeContainerComponent"/> is initialized.
- /// </summary>
- /// <param name="owner">The owning entity.</param>
- public virtual void Initialize(EntityUid owner, IEntityManager entMan)
- {
- Owner = owner;
- }
-
- /// <summary>
- /// How this node will attempt to find other reachable <see cref="Node"/>s to group with.
- /// Returns a set of <see cref="Node"/>s to consider grouping with. Should not return this current <see cref="Node"/>.
- /// </summary>
- /// <remarks>
- /// <para>
- /// The set of nodes returned can be asymmetrical
- /// (meaning that it can return other nodes whose <see cref="GetReachableNodes"/> does not return this node).
- /// If this is used, creation of a new node may not correctly merge networks unless both sides
- /// of this asymmetric relation are made to manually update with <see cref="NodeGroupSystem.QueueReflood"/>.
- /// </para>
- /// </remarks>
- public abstract IEnumerable<Node> GetReachableNodes(TransformComponent xform,
- EntityQuery<NodeContainerComponent> nodeQuery,
- EntityQuery<TransformComponent> xformQuery,
- MapGridComponent? grid,
- IEntityManager entMan);
- }
-}
using System.Diagnostics.CodeAnalysis;
+using Content.Shared.NodeContainer;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
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;
+using Content.Shared.NodeContainer;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
+using Content.Shared.NodeContainer;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
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;
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
{
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;
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;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
using System.Linq;
+using Content.Shared.NodeContainer;
namespace Content.Server.Power.EntitySystems;
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;
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;
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;
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
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
{
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;
using JetBrains.Annotations;
using Robust.Shared.Utility;
using System.Linq;
+using Content.Shared.NodeContainer;
+using Content.Shared.NodeContainer.NodeGroups;
namespace Content.Server.Power.NodeGroups
{
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
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
+using Content.Shared.NodeContainer;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
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;
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
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;
public abstract class AlertsSystem : EntitySystem
{
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private FrozenDictionary<ProtoId<AlertPrototype>, AlertPrototype> _typeToAlert = default!;
return;
}
- ActivateAlert(player.Value, alert);
+ if (ActivateAlert(player.Value, alert) && _timing.IsFirstTimePredicted)
+ {
+ HandledAlert();
+ }
+ }
+
+ protected virtual void HandledAlert()
+ {
+
}
public bool ActivateAlert(EntityUid user, AlertPrototype alert)
--- /dev/null
+using Content.Shared.Body.Components;
+using Content.Shared.Inventory;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Atmos.Components;
+
+/// <summary>
+/// Gas masks or the likes; used by <see cref="InternalsComponent"/> for breathing.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[ComponentProtoName("BreathMask")]
+public sealed partial class BreathToolComponent : Component
+{
+ /// <summary>
+ /// Tool is functional only in allowed slots
+ /// </summary>
+ [DataField]
+ public SlotFlags AllowedSlots = SlotFlags.MASK | SlotFlags.HEAD;
+
+ [ViewVariables]
+ public bool IsFunctional => ConnectedInternalsEntity != null;
+
+ /// <summary>
+ /// Entity that the breath tool is currently connected to.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public EntityUid? ConnectedInternalsEntity;
+}
--- /dev/null
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Atmos.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+public sealed partial class GasTankComponent : Component, IGasMixtureHolder
+{
+ public const float MaxExplosionRange = 26f;
+ private const float DefaultLowPressure = 0f;
+ private const float DefaultOutputPressure = Atmospherics.OneAtmosphere;
+
+ public int Integrity = 3;
+ public bool IsLowPressure => Air.Pressure <= TankLowPressure;
+
+ [DataField]
+ public SoundSpecifier RuptureSound = new SoundPathSpecifier("/Audio/Effects/spray.ogg");
+
+ [DataField]
+ public SoundSpecifier? ConnectSound =
+ new SoundPathSpecifier("/Audio/Effects/internals.ogg")
+ {
+ Params = AudioParams.Default.WithVolume(5f),
+ };
+
+ [DataField]
+ public SoundSpecifier? DisconnectSound;
+
+ // Cancel toggles sounds if we re-toggle again.
+
+ public EntityUid? ConnectStream;
+ public EntityUid? DisconnectStream;
+
+ [DataField]
+ public GasMixture Air { get; set; } = new();
+
+ /// <summary>
+ /// Pressure at which tank should be considered 'low' such as for internals.
+ /// </summary>
+ [DataField]
+ public float TankLowPressure = DefaultLowPressure;
+
+ /// <summary>
+ /// Distributed pressure.
+ /// </summary>
+ [DataField]
+ public float OutputPressure = DefaultOutputPressure;
+
+ /// <summary>
+ /// The maximum allowed output pressure.
+ /// </summary>
+ [DataField]
+ public float MaxOutputPressure = 3 * DefaultOutputPressure;
+
+ /// <summary>
+ /// Tank is connected to internals.
+ /// </summary>
+ [ViewVariables]
+ public bool IsConnected => User != null;
+
+ [DataField, AutoNetworkedField]
+ public EntityUid? User;
+
+ /// <summary>
+ /// True if this entity was recently moved out of a container. This might have been a hand -> inventory
+ /// transfer, or it might have been the user dropping the tank. This indicates the tank needs to be checked.
+ /// </summary>
+ [ViewVariables]
+ public bool CheckUser;
+
+ /// <summary>
+ /// Pressure at which tanks start leaking.
+ /// </summary>
+ [DataField]
+ public float TankLeakPressure = 30 * Atmospherics.OneAtmosphere;
+
+ /// <summary>
+ /// Pressure at which tank spills all contents into atmosphere.
+ /// </summary>
+ [DataField]
+ public float TankRupturePressure = 40 * Atmospherics.OneAtmosphere;
+
+ /// <summary>
+ /// Base 3x3 explosion.
+ /// </summary>
+ [DataField]
+ public float TankFragmentPressure = 50 * Atmospherics.OneAtmosphere;
+
+ /// <summary>
+ /// Increases explosion for each scale kPa above threshold.
+ /// </summary>
+ [DataField]
+ public float TankFragmentScale = 2 * Atmospherics.OneAtmosphere;
+
+ [DataField]
+ public EntProtoId ToggleAction = "ActionToggleInternals";
+
+ [DataField, AutoNetworkedField]
+ public EntityUid? ToggleActionEntity;
+
+ /// <summary>
+ /// Valve to release gas from tank
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public bool IsValveOpen;
+
+ /// <summary>
+ /// Gas release rate in L/s
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float ValveOutputRate = 100f;
+
+ [DataField]
+ public SoundSpecifier ValveSound =
+ new SoundCollectionSpecifier("valveSqueak")
+ {
+ Params = AudioParams.Default.WithVolume(-5f),
+ };
+}
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;
}
--- /dev/null
+using Content.Shared.Atmos.Components;
+using Content.Shared.Body.Components;
+using Content.Shared.Clothing;
+
+namespace Content.Shared.Atmos.EntitySystems;
+
+public abstract partial class SharedAtmosphereSystem
+{
+ private void InitializeBreathTool()
+ {
+ SubscribeLocalEvent<BreathToolComponent, ComponentShutdown>(OnBreathToolShutdown);
+ SubscribeLocalEvent<BreathToolComponent, ItemMaskToggledEvent>(OnMaskToggled);
+ }
+
+ private void OnBreathToolShutdown(Entity<BreathToolComponent> entity, ref ComponentShutdown args)
+ {
+ DisconnectInternals(entity);
+ }
+
+ public void DisconnectInternals(Entity<BreathToolComponent> entity, bool forced = false)
+ {
+ var old = entity.Comp.ConnectedInternalsEntity;
+
+ if (old == null)
+ return;
+
+ entity.Comp.ConnectedInternalsEntity = null;
+
+ if (_internalsQuery.TryComp(old, out var internalsComponent))
+ {
+ _internals.DisconnectBreathTool((old.Value, internalsComponent), entity.Owner, forced: forced);
+ }
+
+ Dirty(entity);
+ }
+
+ private void OnMaskToggled(Entity<BreathToolComponent> ent, ref ItemMaskToggledEvent args)
+ {
+ if (args.Mask.Comp.IsToggled)
+ {
+ DisconnectInternals(ent, forced: true);
+ }
+ else
+ {
+ if (_internalsQuery.TryComp(args.Wearer, out var internals))
+ {
+ _internals.ConnectBreathTool((args.Wearer.Value, internals), ent);
+ }
+ }
+ }
+}
+using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Prototypes;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Systems;
using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
namespace Content.Shared.Atmos.EntitySystems
{
- public abstract class SharedAtmosphereSystem : EntitySystem
+ public abstract partial class SharedAtmosphereSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedInternalsSystem _internals = default!;
+
+ private EntityQuery<InternalsComponent> _internalsQuery;
protected readonly GasPrototype[] GasPrototypes = new GasPrototype[Atmospherics.TotalNumberOfGases];
{
base.Initialize();
+ _internalsQuery = GetEntityQuery<InternalsComponent>();
+
+ InitializeBreathTool();
+
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
GasPrototypes[i] = _prototypeManager.Index<GasPrototype>(i.ToString());
--- /dev/null
+using Content.Shared.Actions;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Body.Systems;
+using Content.Shared.Examine;
+using Content.Shared.Timing;
+using Content.Shared.Toggleable;
+using Content.Shared.UserInterface;
+using Content.Shared.Verbs;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+using InternalsComponent = Content.Shared.Body.Components.InternalsComponent;
+
+namespace Content.Shared.Atmos.EntitySystems;
+
+public abstract class SharedGasTankSystem : EntitySystem
+{
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedContainerSystem _containers = default!;
+ [Dependency] private readonly SharedInternalsSystem _internals = default!;
+ [Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
+ [Dependency] private readonly UseDelaySystem _delay = default!;
+
+ public const string GasTankDelay = "gasTank";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<GasTankComponent, ComponentShutdown>(OnGasShutdown);
+ SubscribeLocalEvent<GasTankComponent, BeforeActivatableUIOpenEvent>(BeforeUiOpen);
+ SubscribeLocalEvent<GasTankComponent, GetItemActionsEvent>(OnGetActions);
+ SubscribeLocalEvent<GasTankComponent, ExaminedEvent>(OnExamined);
+ SubscribeLocalEvent<GasTankComponent, ToggleActionEvent>(OnActionToggle);
+ SubscribeLocalEvent<GasTankComponent, GasTankSetPressureMessage>(OnGasTankSetPressure);
+ SubscribeLocalEvent<GasTankComponent, GasTankToggleInternalsMessage>(OnGasTankToggleInternals);
+ SubscribeLocalEvent<GasTankComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerb);
+ }
+
+ private void OnGasShutdown(Entity<GasTankComponent> gasTank, ref ComponentShutdown args)
+ {
+ DisconnectFromInternals(gasTank);
+ }
+
+ private void OnGasTankToggleInternals(Entity<GasTankComponent> ent, ref GasTankToggleInternalsMessage args)
+ {
+ ToggleInternals(ent, args.Actor);
+ }
+
+ private void OnGasTankSetPressure(Entity<GasTankComponent> ent, ref GasTankSetPressureMessage args)
+ {
+ var pressure = Math.Clamp(args.Pressure, 0f, ent.Comp.MaxOutputPressure);
+
+ ent.Comp.OutputPressure = pressure;
+ Dirty(ent);
+ }
+
+ public virtual void UpdateUserInterface(Entity<GasTankComponent> ent)
+ {
+
+ }
+
+ private void BeforeUiOpen(Entity<GasTankComponent> ent, ref BeforeActivatableUIOpenEvent args)
+ {
+ UpdateUserInterface(ent);
+ }
+
+ private void OnGetActions(EntityUid uid, GasTankComponent component, GetItemActionsEvent args)
+ {
+ args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
+ Dirty(uid, component);
+ }
+
+ private void OnExamined(EntityUid uid, GasTankComponent component, ExaminedEvent args)
+ {
+ using var _ = args.PushGroup(nameof(GasTankComponent));
+
+ if (args.IsInDetailsRange)
+ args.PushMarkup(Loc.GetString("comp-gas-tank-examine", ("pressure", Math.Round(component.Air?.Pressure ?? 0))));
+
+ if (component.IsConnected)
+ args.PushMarkup(Loc.GetString("comp-gas-tank-connected"));
+
+ args.PushMarkup(Loc.GetString(component.IsValveOpen ? "comp-gas-tank-examine-open-valve" : "comp-gas-tank-examine-closed-valve"));
+ }
+
+ private void OnActionToggle(Entity<GasTankComponent> gasTank, ref ToggleActionEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ ToggleInternals(gasTank, user: args.Performer);
+ args.Handled = true;
+ }
+
+ private void OnGetAlternativeVerb(EntityUid uid, GasTankComponent component, GetVerbsEvent<AlternativeVerb> args)
+ {
+ if (!args.CanAccess || !args.CanInteract || args.Hands == null)
+ return;
+
+ args.Verbs.Add(new AlternativeVerb()
+ {
+ Text = component.IsValveOpen ? Loc.GetString("comp-gas-tank-close-valve") : Loc.GetString("comp-gas-tank-open-valve"),
+ Act = () =>
+ {
+ component.IsValveOpen = !component.IsValveOpen;
+ _audio.PlayPredicted(component.ValveSound, uid, args.User);
+ Dirty(uid, component);
+ },
+ Disabled = component.IsConnected,
+ });
+ }
+
+ public bool CanConnectToInternals(Entity<GasTankComponent> ent)
+ {
+ TryGetInternalsComp(ent, out _, out var internalsComp, ent.Comp.User);
+ return internalsComp != null && internalsComp.BreathTools.Count != 0 && !ent.Comp.IsValveOpen;
+ }
+
+ public bool ConnectToInternals(Entity<GasTankComponent> ent, EntityUid? user = null)
+ {
+ var (owner, component) = ent;
+ if (component.IsConnected || !CanConnectToInternals(ent))
+ return false;
+
+ TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, ent.Comp.User);
+ if (internalsUid == null || internalsComp == null)
+ return false;
+
+ if (!_delay.TryResetDelay(ent.Owner, checkDelayed: true, id: GasTankDelay))
+ return false;
+
+ if (_internals.TryConnectTank((internalsUid.Value, internalsComp), owner))
+ component.User = internalsUid.Value;
+
+ Dirty(ent);
+ _actions.SetToggled(component.ToggleActionEntity, component.IsConnected);
+ _actions.SetCooldown(component.ToggleActionEntity, TimeSpan.FromSeconds(1));
+
+ // Couldn't toggle!
+ if (!component.IsConnected)
+ return false;
+
+ component.ConnectStream = _audio.Stop(component.ConnectStream);
+ component.ConnectStream = _audio.PlayPredicted(component.ConnectSound, owner, user)?.Entity;
+ UpdateUserInterface(ent);
+ return true;
+ }
+
+ /// <summary>
+ /// Tries to retrieve the internals component of either the gas tank's user,
+ /// or the gas tank's... containing container
+ /// </summary>
+ /// <param name="user">The user of the gas tank</param>
+ /// <returns>True if internals comp isn't null, false if it is null</returns>
+ private bool TryGetInternalsComp(Entity<GasTankComponent> ent, out EntityUid? internalsUid, out InternalsComponent? internalsComp, EntityUid? user = null)
+ {
+ internalsUid = default;
+ internalsComp = default;
+
+ // If the gas tank doesn't exist for whatever reason, don't even bother
+ if (TerminatingOrDeleted(ent.Owner))
+ return false;
+
+ user ??= ent.Comp.User;
+ // Check if the gas tank's user actually has the component that allows them to use a gas tank and mask
+ if (TryComp<InternalsComponent>(user, out var userInternalsComp))
+ {
+ internalsUid = user;
+ internalsComp = userInternalsComp;
+ return true;
+ }
+
+ // Yeah I have no clue what this actually does, I appreciate the lack of comments on the original function
+ if (_containers.TryGetContainingContainer((ent.Owner, Transform(ent.Owner)), out var container))
+ {
+ if (TryComp<InternalsComponent>(container.Owner, out var containerInternalsComp))
+ {
+ internalsUid = container.Owner;
+ internalsComp = containerInternalsComp;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public bool DisconnectFromInternals(Entity<GasTankComponent> ent, EntityUid? user = null, bool forced = false)
+ {
+ var (owner, component) = ent;
+
+ if (component.User == null)
+ return false;
+
+ if (!forced && !_delay.TryResetDelay(ent.Owner, checkDelayed: true, id: GasTankDelay))
+ return false;
+
+ TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, component.User);
+ component.User = null;
+ Dirty(ent);
+
+ _actions.SetToggled(component.ToggleActionEntity, false);
+
+ // I hate this but actions have no easy way to unify this with usedelay.
+ if (!forced && _delay.TryGetDelayInfo(ent.Owner, out var delayInfo, id: GasTankDelay))
+ {
+ _actions.SetCooldown(component.ToggleActionEntity, delayInfo.Length);
+ }
+
+ if (internalsUid != null && internalsComp != null)
+ _internals.DisconnectTank((internalsUid.Value, internalsComp), forced: forced);
+
+ component.DisconnectStream = _audio.Stop(component.DisconnectStream);
+ component.DisconnectStream = _audio.PlayPredicted(component.DisconnectSound, owner, user)?.Entity;
+ UpdateUserInterface(ent);
+ return true;
+ }
+
+ private bool ToggleInternals(Entity<GasTankComponent> ent, EntityUid? user = null)
+ {
+ if (ent.Comp.IsConnected)
+ {
+ return DisconnectFromInternals(ent, user);
+ }
+ else
+ {
+ return ConnectToInternals(ent, user);
+ }
+ }
+}
--- /dev/null
+namespace Content.Shared.Atmos;
+
+public interface IGasMixtureHolder
+{
+ public GasMixture Air { get; set; }
+}
\ No newline at end of file
/// Useful when there are multiple UI for an object. Here it's future-proofing only.
/// </summary>
[Serializable, NetSerializable]
- public enum GasCanisterUiKey
+ public enum GasCanisterUiKey : byte
{
Key,
}
[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;
}
}
--- /dev/null
+using Content.Shared.Atmos.Piping.Binary.Components;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Guidebook;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Atmos.Piping.Unary.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class GasCanisterComponent : Component, IGasMixtureHolder
+{
+ [DataField("port")]
+ public string PortName { get; set; } = "port";
+
+ /// <summary>
+ /// Container name for the gas tank holder.
+ /// </summary>
+ [DataField("container")]
+ public string ContainerName { get; set; } = "tank_slot";
+
+ [DataField]
+ public ItemSlot GasTankSlot = new();
+
+ [DataField("gasMixture")]
+ public GasMixture Air { get; set; } = new();
+
+ /// <summary>
+ /// Last recorded pressure, for appearance-updating purposes.
+ /// </summary>
+ public float LastPressure = 0f;
+
+ /// <summary>
+ /// Minimum release pressure possible for the release valve.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float MinReleasePressure = Atmospherics.OneAtmosphere / 10;
+
+ /// <summary>
+ /// Maximum release pressure possible for the release valve.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float MaxReleasePressure = Atmospherics.OneAtmosphere * 10;
+
+ /// <summary>
+ /// Valve release pressure.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float ReleasePressure = Atmospherics.OneAtmosphere;
+
+ /// <summary>
+ /// Whether the release valve is open on the canister.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public bool ReleaseValve = false;
+
+ [GuidebookData]
+ public float Volume => Air.Volume;
+}
--- /dev/null
+using Content.Shared.Administration.Logs;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Piping.Binary.Components;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Database;
+using Content.Shared.NodeContainer;
+using Robust.Shared.Containers;
+using GasCanisterComponent = Content.Shared.Atmos.Piping.Unary.Components.GasCanisterComponent;
+
+namespace Content.Shared.Atmos.Piping.Unary.Systems;
+
+public abstract class SharedGasCanisterSystem : EntitySystem
+{
+ [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!;
+ [Dependency] private readonly ItemSlotsSystem _slots = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<GasCanisterComponent, EntInsertedIntoContainerMessage>(OnCanisterContainerModified);
+ SubscribeLocalEvent<GasCanisterComponent, EntRemovedFromContainerMessage>(OnCanisterContainerModified);
+ SubscribeLocalEvent<GasCanisterComponent, ItemSlotInsertAttemptEvent>(OnCanisterInsertAttempt);
+ SubscribeLocalEvent<GasCanisterComponent, ComponentStartup>(OnCanisterStartup);
+
+ // Bound UI subscriptions
+ SubscribeLocalEvent<GasCanisterComponent, GasCanisterHoldingTankEjectMessage>(OnHoldingTankEjectMessage);
+ SubscribeLocalEvent<GasCanisterComponent, GasCanisterChangeReleasePressureMessage>(OnCanisterChangeReleasePressure);
+ SubscribeLocalEvent<GasCanisterComponent, GasCanisterChangeReleaseValveMessage>(OnCanisterChangeReleaseValve);
+ }
+
+ private void OnCanisterStartup(Entity<GasCanisterComponent> ent, ref ComponentStartup args)
+ {
+ // Ensure container
+ _slots.AddItemSlot(ent.Owner, ent.Comp.ContainerName, ent.Comp.GasTankSlot);
+ }
+
+ private void OnCanisterContainerModified(EntityUid uid, GasCanisterComponent component, ContainerModifiedMessage args)
+ {
+ if (args.Container.ID != component.ContainerName)
+ return;
+
+ DirtyUI(uid, component);
+ _appearance.SetData(uid, GasCanisterVisuals.TankInserted, args is EntInsertedIntoContainerMessage);
+ }
+
+ private static string GetContainedGasesString(Entity<GasCanisterComponent> canister)
+ {
+ return string.Join(", ", canister.Comp.Air);
+ }
+
+ private void OnHoldingTankEjectMessage(EntityUid uid, GasCanisterComponent canister, GasCanisterHoldingTankEjectMessage args)
+ {
+ if (canister.GasTankSlot.Item == null)
+ return;
+
+ var item = canister.GasTankSlot.Item;
+ _slots.TryEjectToHands(uid, canister.GasTankSlot, args.Actor, excludeUserAudio: true);
+
+ if (canister.ReleaseValve)
+ {
+ AdminLogger.Add(LogType.CanisterTankEjected, LogImpact.High, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister} while the valve was open, releasing [{GetContainedGasesString((uid, canister))}] to atmosphere");
+ }
+ else
+ {
+ AdminLogger.Add(LogType.CanisterTankEjected, LogImpact.Medium, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister}");
+ }
+
+ if (UI.TryGetUiState<GasCanisterBoundUserInterfaceState>(uid, GasCanisterUiKey.Key, out var lastState))
+ {
+ // We can at least predict 0 pressure for now even without atmos prediction.
+ var newState = new GasCanisterBoundUserInterfaceState(lastState.CanisterPressure, lastState.PortStatus, 0f);
+ UI.SetUiState(uid, GasCanisterUiKey.Key, newState);
+ }
+
+ DirtyUI(uid, canister);
+ }
+
+ private void OnCanisterChangeReleasePressure(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleasePressureMessage args)
+ {
+ var pressure = Math.Clamp(args.Pressure, canister.MinReleasePressure, canister.MaxReleasePressure);
+
+ AdminLogger.Add(LogType.CanisterPressure, LogImpact.Medium, $"{ToPrettyString(args.Actor):player} set the release pressure on {ToPrettyString(uid):canister} to {args.Pressure}");
+
+ canister.ReleasePressure = pressure;
+ Dirty(uid, canister);
+ DirtyUI(uid, canister);
+ }
+
+ private void OnCanisterChangeReleaseValve(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleaseValveMessage args)
+ {
+ // filling a jetpack with plasma is less important than filling a room with it
+ var impact = canister.GasTankSlot.HasItem ? LogImpact.Medium : LogImpact.High;
+
+ var containedGasDict = new Dictionary<Gas, float>();
+ var containedGasArray = Enum.GetValues(typeof(Gas));
+
+ for (var i = 0; i < containedGasArray.Length; i++)
+ {
+ containedGasDict.Add((Gas)i, canister.Air[i]);
+ }
+
+ AdminLogger.Add(LogType.CanisterValve, impact, $"{ToPrettyString(args.Actor):player} set the valve on {ToPrettyString(uid):canister} to {args.Valve:valveState} while it contained [{string.Join(", ", containedGasDict)}]");
+
+ canister.ReleaseValve = args.Valve;
+ Dirty(uid, canister);
+ DirtyUI(uid, canister);
+ }
+
+ private void OnCanisterInsertAttempt(EntityUid uid, GasCanisterComponent component, ref ItemSlotInsertAttemptEvent args)
+ {
+ if (args.Slot.ID != component.ContainerName || args.User == null)
+ return;
+
+ // Could whitelist but we want to check if it's open so.
+ if (!TryComp<GasTankComponent>(args.Item, out var gasTank) || gasTank.IsValveOpen)
+ {
+ args.Cancelled = true;
+ }
+ }
+
+ protected abstract void DirtyUI(EntityUid uid, GasCanisterComponent? component = null, NodeContainerComponent? nodes = null);
+}
--- /dev/null
+using Content.Shared.Alert;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Body.Components;
+
+/// <summary>
+/// Handles hooking up a mask (breathing tool) / gas tank together and allowing the Owner to breathe through it.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+public sealed partial class InternalsComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public EntityUid? GasTankEntity;
+
+ [DataField, AutoNetworkedField]
+ public HashSet<EntityUid> BreathTools = new();
+
+ /// <summary>
+ /// Toggle Internals delay when the target is not you.
+ /// </summary>
+ [DataField]
+ public TimeSpan Delay = TimeSpan.FromSeconds(3);
+
+ [DataField]
+ public ProtoId<AlertPrototype> InternalsAlert = "Internals";
+}
--- /dev/null
+using Content.Shared.Alert;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.EntitySystems;
+using Content.Shared.Body.Components;
+using Content.Shared.DoAfter;
+using Content.Shared.Hands.Components;
+using Content.Shared.Internals;
+using Content.Shared.Inventory;
+using Content.Shared.Popups;
+using Content.Shared.Verbs;
+using Robust.Shared.Containers;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Body.Systems;
+
+/// <summary>
+/// Handles lung breathing with gas tanks for entities.
+/// </summary>
+public abstract class SharedInternalsSystem : EntitySystem
+{
+ [Dependency] private readonly AlertsSystem _alerts = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly SharedGasTankSystem _gasTank = default!;
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<InternalsComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs);
+
+ SubscribeLocalEvent<InternalsComponent, ComponentStartup>(OnInternalsStartup);
+ SubscribeLocalEvent<InternalsComponent, ComponentShutdown>(OnInternalsShutdown);
+
+ SubscribeLocalEvent<InternalsComponent, InternalsDoAfterEvent>(OnDoAfter);
+ SubscribeLocalEvent<InternalsComponent, ToggleInternalsAlertEvent>(OnToggleInternalsAlert);
+ }
+
+ private void OnGetInteractionVerbs(
+ Entity<InternalsComponent> ent,
+ ref GetVerbsEvent<InteractionVerb> args)
+ {
+ if (!args.CanAccess || !args.CanInteract || args.Hands is null)
+ return;
+
+ if (!AreInternalsWorking(ent) && ent.Comp.BreathTools.Count == 0)
+ return;
+
+ var user = args.User;
+
+ InteractionVerb verb = new()
+ {
+ Act = () =>
+ {
+ ToggleInternals(ent, user, force: false, ent);
+ },
+ Message = Loc.GetString("action-description-internals-toggle"),
+ Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/dot.svg.192dpi.png")),
+ Text = Loc.GetString("action-name-internals-toggle"),
+ };
+
+ args.Verbs.Add(verb);
+ }
+
+ public bool ToggleInternals(
+ EntityUid uid,
+ EntityUid user,
+ bool force,
+ InternalsComponent? internals = null)
+ {
+ if (!Resolve(uid, ref internals, logMissing: false))
+ return false;
+
+ // Check if a mask is present.
+ if (internals.BreathTools.Count == 0)
+ {
+ _popupSystem.PopupClient(Loc.GetString("internals-no-breath-tool"), uid, user);
+ return false;
+ }
+
+ // Start the toggle do-after if it's on someone else.
+ if (!force && user != uid)
+ {
+ return StartToggleInternalsDoAfter(user, (uid, internals));
+ }
+
+ // Toggle off.
+ if (TryComp(internals.GasTankEntity, out GasTankComponent? gas))
+ {
+ return _gasTank.DisconnectFromInternals((internals.GasTankEntity.Value, gas), user);
+ }
+ else
+ {
+ // Check if tank is present.
+ var tank = FindBestGasTank(uid);
+
+ // If they're not on then check if we have a mask to use
+ if (tank == null)
+ {
+ _popupSystem.PopupClient(Loc.GetString("internals-no-tank"), uid, user);
+ return false;
+ }
+
+ return _gasTank.ConnectToInternals(tank.Value, user: user);
+ }
+ }
+
+ private bool StartToggleInternalsDoAfter(EntityUid user, Entity<InternalsComponent> targetEnt)
+ {
+ // Is the target not you? If yes, use a do-after to give them time to respond.
+ var isUser = user == targetEnt.Owner;
+ var delay = !isUser ? targetEnt.Comp.Delay : TimeSpan.Zero;
+
+ return _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, delay, new InternalsDoAfterEvent(), targetEnt, target: targetEnt)
+ {
+ BreakOnDamage = true,
+ BreakOnMove = true,
+ MovementThreshold = 0.1f,
+ });
+ }
+
+ private void OnDoAfter(Entity<InternalsComponent> ent, ref InternalsDoAfterEvent args)
+ {
+ if (args.Cancelled || args.Handled)
+ return;
+
+ ToggleInternals(ent, args.User, force: true, ent);
+
+ args.Handled = true;
+ }
+
+ private void OnToggleInternalsAlert(Entity<InternalsComponent> ent, ref ToggleInternalsAlertEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ args.Handled |= ToggleInternals(ent, ent, false, internals: ent.Comp);
+ }
+
+ private void OnInternalsStartup(Entity<InternalsComponent> ent, ref ComponentStartup args)
+ {
+ _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
+ }
+
+ private void OnInternalsShutdown(Entity<InternalsComponent> ent, ref ComponentShutdown args)
+ {
+ _alerts.ClearAlert(ent, ent.Comp.InternalsAlert);
+ }
+
+ public void ConnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
+ {
+ if (!ent.Comp.BreathTools.Add(toolEntity))
+ return;
+
+ if (TryComp(toolEntity, out BreathToolComponent? breathTool))
+ {
+ breathTool.ConnectedInternalsEntity = ent.Owner;
+ Dirty(toolEntity, breathTool);
+ }
+
+ Dirty(ent);
+ _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
+ }
+
+ public void DisconnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity, bool forced = false)
+ {
+ if (!ent.Comp.BreathTools.Remove(toolEntity))
+ return;
+
+ Dirty(ent);
+
+ if (TryComp(toolEntity, out BreathToolComponent? breathTool))
+ {
+ breathTool.ConnectedInternalsEntity = null;
+ Dirty(toolEntity, breathTool);
+ }
+
+ if (ent.Comp.BreathTools.Count == 0)
+ {
+ DisconnectTank(ent, forced: forced);
+ }
+
+ _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
+ }
+
+ public void DisconnectTank(Entity<InternalsComponent> ent, bool forced = false)
+ {
+ if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
+ _gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank), forced: forced);
+
+ ent.Comp.GasTankEntity = null;
+ Dirty(ent);
+ _alerts.ShowAlert(ent.Owner, ent.Comp.InternalsAlert, GetSeverity(ent.Comp));
+ }
+
+ public bool TryConnectTank(Entity<InternalsComponent> ent, EntityUid tankEntity)
+ {
+ if (ent.Comp.BreathTools.Count == 0)
+ return false;
+
+ if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
+ _gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank));
+
+ ent.Comp.GasTankEntity = tankEntity;
+ Dirty(ent);
+ _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
+ return true;
+ }
+
+ public bool AreInternalsWorking(EntityUid uid, InternalsComponent? component = null)
+ {
+ return Resolve(uid, ref component, logMissing: false)
+ && AreInternalsWorking(component);
+ }
+
+ public bool AreInternalsWorking(InternalsComponent component)
+ {
+ return TryComp(component.BreathTools.FirstOrNull(), out BreathToolComponent? breathTool)
+ && breathTool.IsFunctional
+ && HasComp<GasTankComponent>(component.GasTankEntity);
+ }
+
+ protected short GetSeverity(InternalsComponent component)
+ {
+ if (component.BreathTools.Count == 0 || !AreInternalsWorking(component))
+ return 2;
+
+ // If pressure in the tank is below low pressure threshold, flash warning on internals UI
+ if (TryComp<GasTankComponent>(component.GasTankEntity, out var gasTank)
+ && gasTank.IsLowPressure)
+ {
+ return 0;
+ }
+
+ return 1;
+ }
+
+ public Entity<GasTankComponent>? FindBestGasTank(
+ Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user)
+ {
+ // TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses
+ // Prioritise
+ // 1. back equipped tanks
+ // 2. exo-slot tanks
+ // 3. in-hand tanks
+ // 4. pocket/belt tanks
+
+ if (!Resolve(user, ref user.Comp2, ref user.Comp3))
+ return null;
+
+ if (_inventory.TryGetSlotEntity(user, "back", out var backEntity, user.Comp2, user.Comp3) &&
+ TryComp<GasTankComponent>(backEntity, out var backGasTank) &&
+ _gasTank.CanConnectToInternals((backEntity.Value, backGasTank)))
+ {
+ return (backEntity.Value, backGasTank);
+ }
+
+ if (_inventory.TryGetSlotEntity(user, "suitstorage", out var entity, user.Comp2, user.Comp3) &&
+ TryComp<GasTankComponent>(entity, out var gasTank) &&
+ _gasTank.CanConnectToInternals((entity.Value, gasTank)))
+ {
+ return (entity.Value, gasTank);
+ }
+
+ foreach (var item in _inventory.GetHandOrInventoryEntities((user.Owner, user.Comp1, user.Comp2)))
+ {
+ if (TryComp(item, out gasTank) && _gasTank.CanConnectToInternals((item, gasTank)))
+ return (item, gasTank);
+ }
+
+ return null;
+ }
+}
private void OnGetActions(EntityUid uid, MaskComponent component, GetItemActionsEvent args)
{
if (_inventorySystem.InSlotWithFlags(uid, SlotFlags.MASK))
+ {
args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
+ Dirty(uid, component);
+ }
}
private void OnToggleMask(Entity<MaskComponent> ent, ref ToggleMaskEvent args)
private void OnGotUnequipped(EntityUid uid, MaskComponent mask, GotUnequippedEvent args)
{
- // Masks are currently always un-toggled when unequipped.
- SetToggled((uid, mask), false);
+ if (!mask.IsToggled || !mask.IsToggleable)
+ return;
+
+ mask.IsToggled = false;
+ ToggleMaskComponents(uid, mask, args.Equipee, mask.EquippedPrefix, true);
+ }
+
+ /// <summary>
+ /// Called after setting IsToggled, raises events and dirties.
+ /// </summary>
+ private void ToggleMaskComponents(EntityUid uid, MaskComponent mask, EntityUid wearer, string? equippedPrefix = null, bool isEquip = false)
+ {
+ Dirty(uid, mask);
+ if (mask.ToggleActionEntity is {} action)
+ _actionSystem.SetToggled(action, mask.IsToggled);
+
+ var maskEv = new ItemMaskToggledEvent((wearer, mask), wearer);
+ RaiseLocalEvent(uid, ref maskEv);
+
+ var wearerEv = new WearerMaskToggledEvent((wearer, mask));
+ RaiseLocalEvent(wearer, ref wearerEv);
}
private void OnFolded(Entity<MaskComponent> ent, ref FoldedEvent args)
+using Robust.Shared.Audio;
using Robust.Shared.GameStates;
namespace Content.Shared.Lock;
/// </summary>
[DataField]
public bool RequireLocked;
+
+ /// <summary>
+ /// Sound to be played if an attempt is blocked.
+ /// </summary>
+ [DataField]
+ public SoundSpecifier? AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
}
base.Initialize();
SubscribeLocalEvent<LockComponent, ComponentStartup>(OnStartup);
- SubscribeLocalEvent<LockComponent, ActivateInWorldEvent>(OnActivated);
+ SubscribeLocalEvent<LockComponent, ActivateInWorldEvent>(OnActivated, before: [typeof(ActivatableUISystem)]);
SubscribeLocalEvent<LockComponent, StorageOpenAttemptEvent>(OnStorageOpenAttempt);
SubscribeLocalEvent<LockComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<LockComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleLockVerb);
// 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);
}
}
{
args.Cancel();
if (lockComp.Locked)
+ {
_sharedPopupSystem.PopupClient(Loc.GetString("entity-storage-component-locked-message"), uid, args.User);
+ }
+
+ _audio.PlayPredicted(component.AccessDeniedSound, uid, args.User);
}
}
--- /dev/null
+using Content.Shared.NodeContainer.NodeGroups;
+using Robust.Shared.Map.Components;
+
+namespace Content.Shared.NodeContainer;
+
+/// <summary>
+/// Organizes themselves into distinct <see cref="INodeGroup"/>s with other <see cref="Node"/>s
+/// that they can "reach" and have the same <see cref="Node.NodeGroupID"/>.
+/// </summary>
+[ImplicitDataDefinitionForInheritors]
+public abstract partial class Node
+{
+ /// <summary>
+ /// An ID used as a criteria for combining into groups. Determines which <see cref="INodeGroup"/>
+ /// implementation is used as a group, detailed in <see cref="INodeGroupFactory"/>.
+ /// </summary>
+ [DataField("nodeGroupID")]
+ public NodeGroupID NodeGroupID { get; private set; } = NodeGroupID.Default;
+
+ /// <summary>
+ /// The node group this node is a part of.
+ /// </summary>
+ [ViewVariables] public INodeGroup? NodeGroup;
+
+ /// <summary>
+ /// The entity that owns this node via its <see cref="NodeContainerComponent"/>.
+ /// </summary>
+ [ViewVariables] public EntityUid Owner { get; private set; } = default!;
+
+ /// <summary>
+ /// If this node should be considered for connection by other nodes.
+ /// </summary>
+ public virtual bool Connectable(IEntityManager entMan, TransformComponent? xform = null)
+ {
+ if (Deleting)
+ return false;
+
+ if (entMan.IsQueuedForDeletion(Owner))
+ return false;
+
+ if (!NeedAnchored)
+ return true;
+
+ xform ??= entMan.GetComponent<TransformComponent>(Owner);
+ return xform.Anchored;
+ }
+
+ [DataField]
+ public bool NeedAnchored { get; private set; } = true;
+
+ public virtual void OnAnchorStateChanged(IEntityManager entityManager, bool anchored) { }
+
+ /// <summary>
+ /// Prevents a node from being used by other nodes while midway through removal.
+ /// </summary>
+ public bool Deleting;
+
+ /// <summary>
+ /// All compatible nodes that are reachable by this node.
+ /// Effectively, active connections out of this node.
+ /// </summary>
+ public readonly HashSet<Node> ReachableNodes = new();
+
+ public int FloodGen;
+ public int UndirectGen;
+ public bool FlaggedForFlood;
+ public int NetId;
+
+ /// <summary>
+ /// Name of this node on the owning <see cref="NodeContainerComponent"/>.
+ /// </summary>
+ public string Name = default!;
+
+ /// <summary>
+ /// Invoked when the owning <see cref="NodeContainerComponent"/> is initialized.
+ /// </summary>
+ /// <param name="owner">The owning entity.</param>
+ public virtual void Initialize(EntityUid owner, IEntityManager entMan)
+ {
+ Owner = owner;
+ }
+
+ /// <summary>
+ /// How this node will attempt to find other reachable <see cref="Node"/>s to group with.
+ /// Returns a set of <see cref="Node"/>s to consider grouping with. Should not return this current <see cref="Node"/>.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The set of nodes returned can be asymmetrical
+ /// (meaning that it can return other nodes whose <see cref="GetReachableNodes"/> does not return this node).
+ /// If this is used, creation of a new node may not correctly merge networks unless both sides
+ /// of this asymmetric relation are made to manually update with <see cref="NodeGroupSystem.QueueReflood"/>.
+ /// </para>
+ /// </remarks>
+ public abstract IEnumerable<Node> GetReachableNodes(TransformComponent xform,
+ EntityQuery<NodeContainerComponent> nodeQuery,
+ EntityQuery<TransformComponent> xformQuery,
+ MapGridComponent? grid,
+ IEntityManager entMan);
+}
--- /dev/null
+using Robust.Shared.GameStates;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.NodeContainer;
+
+/// <summary>
+/// Creates and maintains a set of <see cref="Rope.Node"/>s.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class NodeContainerComponent : Component
+{
+ //HACK: THIS BEING readOnly IS A FILTHY HACK AND I HATE IT --moony
+ [DataField(readOnly: true, serverOnly: true)] public Dictionary<string, Node> Nodes { get; private set; } = new();
+
+ [DataField] public bool Examinable = false;
+}
--- /dev/null
+using System.Linq;
+
+namespace Content.Shared.NodeContainer.NodeGroups;
+
+/// <summary>
+/// Maintains a collection of <see cref="Node"/>s, and performs operations requiring a list of
+/// all connected <see cref="Node"/>s.
+/// </summary>
+public interface INodeGroup
+{
+ bool Remaking { get; }
+
+ /// <summary>
+ /// The list of nodes currently in this group.
+ /// </summary>
+ IReadOnlyList<Node> Nodes { get; }
+
+ void Create(NodeGroupID groupId);
+
+ void Initialize(Node sourceNode, IEntityManager entMan);
+
+ void RemoveNode(Node node);
+
+ void LoadNodes(List<Node> groupNodes);
+
+ // In theory, the SS13 curse ensures this method will never be called.
+ void AfterRemake(IEnumerable<IGrouping<INodeGroup?, Node>> newGroups);
+
+ /// <summary>
+ /// Return any additional data to display for the node-visualizer debug overlay.
+ /// </summary>
+ string? GetDebugData();
+}
\ No newline at end of file
--- /dev/null
+namespace Content.Shared.NodeContainer.NodeGroups;
+
+public enum NodeGroupID : byte
+{
+ Default,
+ HVPower,
+ MVPower,
+ Apc,
+ AMEngine,
+ Pipe,
+ WireNet,
+
+ /// <summary>
+ /// Group used by the TEG.
+ /// </summary>
+ /// <seealso cref="Content.Server.Power.Generation.Teg.TegSystem"/>
+ /// <seealso cref="Content.Server.Power.Generation.Teg.TegNodeGroup"/>
+ Teg,
+}
/// </summary>
public bool IsDelayed(Entity<UseDelayComponent?> ent, string id = DefaultId)
{
- if (!Resolve(ent, ref ent.Comp, false))
+ if (!Resolve(ent.Owner, ref ent.Comp, false))
return false;
if (!ent.Comp.Delays.TryGetValue(id, out var entry))
/// <param name="info"></param>
/// <param name="id"></param>
/// <returns></returns>
- public bool TryGetDelayInfo(Entity<UseDelayComponent> ent, [NotNullWhen(true)] out UseDelayInfo? info, string id = DefaultId)
+ public bool TryGetDelayInfo(Entity<UseDelayComponent?> ent, [NotNullWhen(true)] out UseDelayInfo? info, string id = DefaultId)
{
+ if (!Resolve(ent.Owner, ref ent.Comp, false))
+ {
+ info = null;
+ return false;
+ }
+
return ent.Comp.Delays.TryGetValue(id, out info);
}
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();
}
}
slots:
- Back
- suitStorage
+ - type: UseDelay
+ delays:
+ gasTank:
+ length: 1.0
- type: ActivatableUI
key: enum.SharedGasTankUiKey.Key
- type: UserInterface
interfaces:
enum.SharedGasTankUiKey.Key:
type: GasTankBoundUserInterface
+ - type: UseDelay
+ delays:
+ gasTank:
+ length: 1.0
- type: Clothing
sprite: Objects/Tanks/Jetpacks/blue.rsi
quickEquip: false
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: