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.Throwing; using Content.Shared.Toggleable; using Content.Shared.Verbs; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Random; using Robust.Shared.Configuration; using Content.Shared.CCVar; namespace Content.Server.Atmos.EntitySystems { [UsedImplicitly] public sealed class GasTankSystem : EntitySystem { [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!; [Dependency] private readonly IConfigurationManager _cfg = default!; private const float TimerDelay = 0.5f; private float _timer = 0f; private const float MinimumSoundValvePressure = 10.0f; private float _maxExplosionRange; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnGasShutdown); SubscribeLocalEvent(BeforeUiOpen); SubscribeLocalEvent(OnGetActions); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnActionToggle); SubscribeLocalEvent(OnParentChange); SubscribeLocalEvent(OnGasTankSetPressure); SubscribeLocalEvent(OnGasTankToggleInternals); SubscribeLocalEvent(OnAnalyzed); SubscribeLocalEvent(OnGasTankPrice); SubscribeLocalEvent>(OnGetAlternativeVerb); Subs.CVar(_cfg, CCVars.AtmosTankFragment, UpdateMaxRange, true); } private void UpdateMaxRange(float value) { _maxExplosionRange = value; } private void OnGasShutdown(Entity gasTank, ref ComponentShutdown args) { DisconnectFromInternals(gasTank); } private void OnGasTankToggleInternals(Entity ent, ref GasTankToggleInternalsMessage args) { ToggleInternals(ent); } private void OnGasTankSetPressure(Entity ent, ref GasTankSetPressureMessage args) { var pressure = Math.Clamp(args.Pressure, 0f, ent.Comp.MaxOutputPressure); ent.Comp.OutputPressure = pressure; UpdateUserInterface(ent, true); } public void UpdateUserInterface(Entity ent, bool initialUpdate = false) { var (owner, component) = ent; _ui.SetUiState(owner, SharedGasTankUiKey.Key, new GasTankBoundUserInterfaceState { TankPressure = component.Air?.Pressure ?? 0, OutputPressure = initialUpdate ? component.OutputPressure : null, InternalsConnected = component.IsConnected, CanConnectInternals = CanConnectToInternals(ent) }); } private void BeforeUiOpen(Entity ent, ref BeforeActivatableUIOpenEvent args) { // Only initial update includes output pressure information, to avoid overwriting client-input as the updates come in. UpdateUserInterface(ent, true); } private void OnParentChange(EntityUid uid, GasTankComponent component, ref EntParentChangedMessage args) { // When an item is moved from hands -> pockets, the container removal briefly dumps the item on the floor. // So this is a shitty fix, where the parent check is just delayed. But this really needs to get fixed // properly at some point. component.CheckUser = true; } private void OnGetActions(EntityUid uid, GasTankComponent component, GetItemActionsEvent args) { args.AddAction(ref component.ToggleActionEntity, component.ToggleAction); } private void OnExamined(EntityUid uid, GasTankComponent component, ExaminedEvent args) { using var _ = args.PushGroup(nameof(GasTankComponent)); if (args.IsInDetailsRange) args.PushMarkup(Loc.GetString("comp-gas-tank-examine", ("pressure", Math.Round(component.Air?.Pressure ?? 0)))); if (component.IsConnected) args.PushMarkup(Loc.GetString("comp-gas-tank-connected")); args.PushMarkup(Loc.GetString(component.IsValveOpen ? "comp-gas-tank-examine-open-valve" : "comp-gas-tank-examine-closed-valve")); } private void OnActionToggle(Entity gasTank, ref ToggleActionEvent args) { if (args.Handled) return; ToggleInternals(gasTank); args.Handled = true; } public override void Update(float frameTime) { base.Update(frameTime); _timer += frameTime; if (_timer < TimerDelay) return; _timer -= TimerDelay; var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { var gasTank = (uid, comp); if (comp.IsValveOpen && !comp.IsLowPressure && comp.OutputPressure > 0) { ReleaseGas(gasTank); } if (comp.CheckUser) { comp.CheckUser = false; if (Transform(uid).ParentUid != comp.User) { DisconnectFromInternals(gasTank); continue; } } if (comp.Air != null) { _atmosphereSystem.React(comp.Air, comp); } CheckStatus(gasTank); if (_ui.IsUiOpen(uid, SharedGasTankUiKey.Key)) { UpdateUserInterface(gasTank); } } } private void ReleaseGas(Entity gasTank) { var removed = RemoveAirVolume(gasTank, gasTank.Comp.ValveOutputRate * TimerDelay); var environment = _atmosphereSystem.GetContainingMixture(gasTank.Owner, false, true); if (environment != null) { _atmosphereSystem.Merge(environment, removed); } var strength = removed.TotalMoles * MathF.Sqrt(removed.Temperature); var dir = _random.NextAngle().ToWorldVec(); _throwing.TryThrow(gasTank, dir * strength, strength); if (gasTank.Comp.OutputPressure >= MinimumSoundValvePressure) _audioSys.PlayPvs(gasTank.Comp.RuptureSound, gasTank); } private void ToggleInternals(Entity ent) { if (ent.Comp.IsConnected) { DisconnectFromInternals(ent); } else { ConnectToInternals(ent); } } public GasMixture? RemoveAir(Entity gasTank, float amount) { var gas = gasTank.Comp.Air?.Remove(amount); CheckStatus(gasTank); return gas; } public GasMixture RemoveAirVolume(Entity gasTank, float volume) { var component = gasTank.Comp; if (component.Air == null) return new GasMixture(volume); var molesNeeded = component.OutputPressure * volume / (Atmospherics.R * component.Air.Temperature); var air = RemoveAir(gasTank, molesNeeded); if (air != null) air.Volume = volume; else return new GasMixture(volume); return air; } public bool CanConnectToInternals(Entity ent) { TryGetInternalsComp(ent, out _, out var internalsComp, ent.Comp.User); return internalsComp != null && internalsComp.BreathTools.Count != 0 && !ent.Comp.IsValveOpen; } public void ConnectToInternals(Entity ent) { var (owner, component) = ent; if (component.IsConnected || !CanConnectToInternals(ent)) return; TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, ent.Comp.User); if (internalsUid == null || internalsComp == null) return; if (_internals.TryConnectTank((internalsUid.Value, internalsComp), owner)) component.User = internalsUid.Value; _actions.SetToggled(component.ToggleActionEntity, component.IsConnected); // Couldn't toggle! if (!component.IsConnected) return; component.ConnectStream = _audioSys.Stop(component.ConnectStream); component.ConnectStream = _audioSys.PlayPvs(component.ConnectSound, owner)?.Entity; UpdateUserInterface(ent); } public void DisconnectFromInternals(Entity ent) { var (owner, component) = ent; if (component.User == null) return; TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, component.User); component.User = null; _actions.SetToggled(component.ToggleActionEntity, false); if (internalsUid != null && internalsComp != null) _internals.DisconnectTank((internalsUid.Value, internalsComp)); component.DisconnectStream = _audioSys.Stop(component.DisconnectStream); component.DisconnectStream = _audioSys.PlayPvs(component.DisconnectSound, owner)?.Entity; UpdateUserInterface(ent); } /// /// Tries to retrieve the internals component of either the gas tank's user, /// or the gas tank's... containing container /// /// The user of the gas tank /// True if internals comp isn't null, false if it is null private bool TryGetInternalsComp(Entity ent, out EntityUid? internalsUid, out InternalsComponent? internalsComp, EntityUid? user = null) { internalsUid = default; internalsComp = default; // If the gas tank doesn't exist for whatever reason, don't even bother if (TerminatingOrDeleted(ent.Owner)) return false; user ??= ent.Comp.User; // Check if the gas tank's user actually has the component that allows them to use a gas tank and mask if (TryComp(user, out var userInternalsComp) && userInternalsComp != null) { internalsUid = user; internalsComp = userInternalsComp; return true; } // Yeah I have no clue what this actually does, I appreciate the lack of comments on the original function if (_containers.TryGetContainingContainer((ent.Owner, Transform(ent.Owner)), out var container) && container != null) { if (TryComp(container.Owner, out var containerInternalsComp) && containerInternalsComp != null) { internalsUid = container.Owner; internalsComp = containerInternalsComp; return true; } } return false; } public void AssumeAir(Entity ent, GasMixture giver) { _atmosphereSystem.Merge(ent.Comp.Air, giver); CheckStatus(ent); } public void CheckStatus(Entity ent) { var (owner, component) = ent; if (component.Air == null) return; var pressure = component.Air.Pressure; if (pressure > component.TankFragmentPressure && _maxExplosionRange > 0) { // Give the gas a chance to build up more pressure. for (var i = 0; i < 3; i++) { _atmosphereSystem.React(component.Air, component); } pressure = component.Air.Pressure; var range = MathF.Sqrt((pressure - component.TankFragmentPressure) / component.TankFragmentScale); // Let's cap the explosion, yeah? // !1984 range = Math.Min(Math.Min(range, GasTankComponent.MaxExplosionRange), _maxExplosionRange); _explosions.TriggerExplosive(owner, radius: range); return; } if (pressure > component.TankRupturePressure) { if (component.Integrity <= 0) { var environment = _atmosphereSystem.GetContainingMixture(owner, false, true); if (environment != null) _atmosphereSystem.Merge(environment, component.Air); _audioSys.PlayPvs(component.RuptureSound, Transform(owner).Coordinates, AudioParams.Default.WithVariation(0.125f)); QueueDel(owner); return; } component.Integrity--; return; } if (pressure > component.TankLeakPressure) { if (component.Integrity <= 0) { var environment = _atmosphereSystem.GetContainingMixture(owner, false, true); if (environment == null) return; var leakedGas = component.Air.RemoveRatio(0.25f); _atmosphereSystem.Merge(environment, leakedGas); } else { component.Integrity--; } return; } if (component.Integrity < 3) component.Integrity++; } /// /// Returns the gas mixture for the gas analyzer /// private void OnAnalyzed(EntityUid uid, GasTankComponent component, GasAnalyzerScanEvent args) { args.GasMixtures ??= new List<(string, GasMixture?)>(); args.GasMixtures.Add((Name(uid), component.Air)); } private void OnGasTankPrice(EntityUid uid, GasTankComponent component, ref PriceCalculationEvent args) { args.Price += _atmosphereSystem.GetPrice(component.Air); } private void OnGetAlternativeVerb(EntityUid uid, GasTankComponent component, GetVerbsEvent args) { if (!args.CanAccess || !args.CanInteract || args.Hands == null) return; args.Verbs.Add(new AlternativeVerb() { Text = component.IsValveOpen ? Loc.GetString("comp-gas-tank-close-valve") : Loc.GetString("comp-gas-tank-open-valve"), Act = () => { component.IsValveOpen = !component.IsValveOpen; _audioSys.PlayPvs(component.ValveSound, uid); }, Disabled = component.IsConnected, }); } } }