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