Files
wwdpublic/Content.Server/Atmos/EntitySystems/GasTankSystem.cs
VMSolidus ee5bc2b261 Stop Ectoplasms Complaining (#2245)
# Description

This PR adds a mechanic to various hardsuits and tacsuits, which can now
include optional features such as Integrated Thrusters, or Integrated
Magboots. All the syndicate tacsuits and "Advanced" crew tacsuits have
both. Hardsuits typically have one or the other, rarely both. Vacsuits
never have either, get a jetpack nerd.

While I was at it, I fixed various bugs making these features obnoxious
to interact with, such as hardsuits helmet toggles not showing the
helmet, or turning on your integrated magboots also causing you to start
sucking the air out of your integrated thrusters.

Finally to address players incessantly complaining that "waaah space
wind is too unfair it keeps me in the air too long", I lowered the
maximum time player characters can go without being updated by space
wind to 1 second, down from 2. Base items are untouched, so they'll
remain in the air as per normal.

# Changelog

🆑
- add: Added "Integrated Thrusters" and "Integrated Magboots" as
features that hardsuits and tacsuits can sometimes have. Hardsuits
rarely have both, and sometimes have neither, while the more "Advanced"
Tacsuits such as the CSA series of suits will always have both.
- tweak: Reduced the maximum amount of time player characters can remain
in the air without being touched by space wind again to 1 second, down
from 2. This does nothing if you're actively being thrown.
- fix: Fixed a bug where the hardsuit action to toggle your helmet
wasn't displaying the helmet icon.

(cherry picked from commit b6be634252745fa61e3db3094d94c7f3c234a094)
2025-04-18 19:47:45 +03:00

391 lines
15 KiB
C#

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;
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!;
private const float TimerDelay = 0.5f;
private float _timer = 0f;
private const float MinimumSoundValvePressure = 10.0f;
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);
}
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)
{
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(component)
});
}
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.
// 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(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);
_timer += frameTime;
if (_timer < TimerDelay)
return;
_timer -= TimerDelay;
var query = EntityQueryEnumerator<GasTankComponent>();
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<GasTankComponent> 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<GasTankComponent> ent)
{
if (!ent.Comp.IsInternals)
return;
if (ent.Comp.IsConnected)
{
DisconnectFromInternals(ent);
}
else
{
ConnectToInternals(ent);
}
}
public GasMixture? RemoveAir(Entity<GasTankComponent> gasTank, float amount)
{
var gas = gasTank.Comp.Air?.Remove(amount);
CheckStatus(gasTank);
return gas;
}
public GasMixture RemoveAirVolume(Entity<GasTankComponent> 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(GasTankComponent component)
{
var internals = GetInternalsComponent(component, component.User);
return component.IsInternals && internals != null && internals.BreathTools.Count != 0 && !component.IsValveOpen;
}
public void ConnectToInternals(Entity<GasTankComponent> ent)
{
var (owner, component) = ent;
if (component.IsConnected || !CanConnectToInternals(component))
return;
var internals = GetInternalsComponent(component);
if (internals == null)
return;
if (_internals.TryConnectTank((internals.Owner, internals), owner))
component.User = internals.Owner;
_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, component.Owner)?.Entity;
UpdateUserInterface(ent);
}
public void DisconnectFromInternals(Entity<GasTankComponent> ent)
{
var (owner, component) = ent;
if (component.User == null || !ent.Comp.IsInternals)
return;
var internals = GetInternalsComponent(component);
component.User = null;
_actions.SetToggled(component.ToggleActionEntity, false);
_internals.DisconnectTank(internals);
component.DisconnectStream = _audioSys.Stop(component.DisconnectStream);
component.DisconnectStream = _audioSys.PlayPvs(component.DisconnectSound, component.Owner)?.Entity;
UpdateUserInterface(ent);
}
private InternalsComponent? GetInternalsComponent(GasTankComponent component, EntityUid? owner = null)
{
owner ??= component.User;
if (Deleted(component.Owner))return null;
if (owner != null) return CompOrNull<InternalsComponent>(owner.Value);
return _containers.TryGetContainingContainer(component.Owner, out var container)
? CompOrNull<InternalsComponent>(container.Owner)
: null;
}
public void AssumeAir(Entity<GasTankComponent> ent, GasMixture giver)
{
_atmosphereSystem.Merge(ent.Comp.Air, giver);
CheckStatus(ent);
}
public void CheckStatus(Entity<GasTankComponent> ent)
{
var (owner, component) = ent;
if (component.Air == null)
return;
var pressure = component.Air.Pressure;
if (pressure > component.TankFragmentPressure)
{
// Give the gas a chance to build up more pressure.
for (var i = 0; i < 3; i++)
{
_atmosphereSystem.React(component.Air, component);
}
pressure = component.Air.Pressure;
var range = MathF.Sqrt((pressure - component.TankFragmentPressure) / component.TankFragmentScale);
// Let's cap the explosion, yeah?
// !1984
if (range > GasTankComponent.MaxExplosionRange)
{
range = GasTankComponent.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(component.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++;
}
/// <summary>
/// Returns the gas mixture for the gas analyzer
/// </summary>
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<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,
});
}
}
}