* Add RemoveVolume()
RemoveVolume(vol) captures the common pattern of:
air.RemoveRatio(vol / air.Volume)
Change existing code to use this method where appropriate.
* Add gas recyclers
Gas recyclers catalyze the conversion of CO2 and N2O to O2 and N2. The
gas recycler component takes waste gas from the input net and releases
the result into the output net.
To make things more fun, the input net must be pressurized to 3 MPa and
heated to at least 300 C; otherwise, no reaction will occur.
Game-mechanic wise, gas recyclers contain the catalyst for the
conversion reaction, and therefore, requires no external power. However,
the external pumps and heaters required to make the reaction happen
still do.
* Fix gas recyclers
Fix negative sqrt, fix pressure check after remove.
221 lines
8.2 KiB
C#
221 lines
8.2 KiB
C#
using Content.Server.Administration.Logs;
|
|
using Content.Server.Atmos;
|
|
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.Body.Components;
|
|
using Content.Shared.Damage;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.MobState.Components;
|
|
using Content.Shared.MobState.EntitySystems;
|
|
using JetBrains.Annotations;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Timing;
|
|
|
|
namespace Content.Server.Body.Systems
|
|
{
|
|
[UsedImplicitly]
|
|
public sealed class RespiratorSystem : EntitySystem
|
|
{
|
|
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
|
|
[Dependency] private readonly AtmosphereSystem _atmosSys = default!;
|
|
[Dependency] private readonly BodySystem _bodySystem = default!;
|
|
[Dependency] private readonly DamageableSystem _damageableSys = default!;
|
|
[Dependency] private readonly LungSystem _lungSystem = default!;
|
|
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
|
[Dependency] private readonly SharedMobStateSystem _mobState = default!;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
// We want to process lung reagents before we inhale new reagents.
|
|
UpdatesAfter.Add(typeof(MetabolizerSystem));
|
|
SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
|
|
foreach (var (respirator, body) in
|
|
EntityManager.EntityQuery<RespiratorComponent, SharedBodyComponent>())
|
|
{
|
|
var uid = respirator.Owner;
|
|
|
|
if (_mobState.IsDead(uid))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
respirator.AccumulatedFrametime += frameTime;
|
|
|
|
if (respirator.AccumulatedFrametime < respirator.CycleDelay)
|
|
continue;
|
|
respirator.AccumulatedFrametime -= respirator.CycleDelay;
|
|
UpdateSaturation(respirator.Owner, -respirator.CycleDelay, respirator);
|
|
|
|
if (!_mobState.IsIncapacitated(uid)) // cannot breathe in crit.
|
|
{
|
|
switch (respirator.Status)
|
|
{
|
|
case RespiratorStatus.Inhaling:
|
|
Inhale(uid, body);
|
|
respirator.Status = RespiratorStatus.Exhaling;
|
|
break;
|
|
case RespiratorStatus.Exhaling:
|
|
Exhale(uid, body);
|
|
respirator.Status = RespiratorStatus.Inhaling;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (respirator.Saturation < respirator.SuffocationThreshold)
|
|
{
|
|
if (_gameTiming.CurTime >= respirator.LastGaspPopupTime + respirator.GaspPopupCooldown)
|
|
{
|
|
respirator.LastGaspPopupTime = _gameTiming.CurTime;
|
|
_popupSystem.PopupEntity(Loc.GetString("lung-behavior-gasp"), uid, Filter.Pvs(uid));
|
|
}
|
|
|
|
TakeSuffocationDamage(uid, respirator);
|
|
respirator.SuffocationCycles += 1;
|
|
continue;
|
|
}
|
|
|
|
StopSuffocation(uid, respirator);
|
|
respirator.SuffocationCycles = 0;
|
|
}
|
|
}
|
|
public void Inhale(EntityUid uid, SharedBodyComponent? body=null)
|
|
{
|
|
if (!Resolve(uid, ref body, false))
|
|
return;
|
|
|
|
var organs = _bodySystem.GetComponentsOnMechanisms<LungComponent>(uid, body);
|
|
|
|
// Inhale gas
|
|
var ev = new InhaleLocationEvent();
|
|
RaiseLocalEvent(uid, ev, false);
|
|
|
|
ev.Gas ??= _atmosSys.GetContainingMixture(uid, false, true);
|
|
|
|
if (ev.Gas == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var actualGas = ev.Gas.RemoveVolume(Atmospherics.BreathVolume);
|
|
|
|
var lungRatio = 1.0f / organs.Count;
|
|
var gas = organs.Count == 1 ? actualGas : actualGas.RemoveRatio(lungRatio);
|
|
foreach (var (lung, _) in organs)
|
|
{
|
|
// Merge doesn't remove gas from the giver.
|
|
_atmosSys.Merge(lung.Air, gas);
|
|
_lungSystem.GasToReagent(lung.Owner, lung);
|
|
}
|
|
}
|
|
|
|
public void Exhale(EntityUid uid, SharedBodyComponent? body=null)
|
|
{
|
|
if (!Resolve(uid, ref body, false))
|
|
return;
|
|
|
|
var organs = _bodySystem.GetComponentsOnMechanisms<LungComponent>(uid, body).ToArray();
|
|
|
|
// exhale gas
|
|
|
|
var ev = new ExhaleLocationEvent();
|
|
RaiseLocalEvent(uid, ev, false);
|
|
|
|
if (ev.Gas == null)
|
|
{
|
|
ev.Gas = _atmosSys.GetContainingMixture(uid, false, true);
|
|
|
|
// Walls and grids without atmos comp return null. I guess it makes sense to not be able to exhale in walls,
|
|
// but this also means you cannot exhale on some grids.
|
|
ev.Gas ??= GasMixture.SpaceGas;
|
|
}
|
|
|
|
var outGas = new GasMixture(ev.Gas.Volume);
|
|
foreach (var (lung, _) in organs)
|
|
{
|
|
_atmosSys.Merge(outGas, lung.Air);
|
|
lung.Air.Clear();
|
|
lung.LungSolution.RemoveAllSolution();
|
|
}
|
|
|
|
_atmosSys.Merge(ev.Gas, outGas);
|
|
}
|
|
|
|
private void TakeSuffocationDamage(EntityUid uid, RespiratorComponent respirator)
|
|
{
|
|
if (respirator.SuffocationCycles == 2)
|
|
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(uid):entity} started suffocating");
|
|
|
|
if (respirator.SuffocationCycles >= respirator.SuffocationCycleThreshold)
|
|
{
|
|
_alertsSystem.ShowAlert(uid, AlertType.LowOxygen);
|
|
}
|
|
|
|
_damageableSys.TryChangeDamage(uid, respirator.Damage, true, false);
|
|
}
|
|
|
|
private void StopSuffocation(EntityUid uid, RespiratorComponent respirator)
|
|
{
|
|
if (respirator.SuffocationCycles >= 2)
|
|
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(uid):entity} stopped suffocating");
|
|
|
|
_alertsSystem.ClearAlert(uid, AlertType.LowOxygen);
|
|
|
|
_damageableSys.TryChangeDamage(uid, respirator.DamageRecovery, true);
|
|
}
|
|
|
|
public void UpdateSaturation(EntityUid uid, float amount,
|
|
RespiratorComponent? respirator = null)
|
|
{
|
|
if (!Resolve(uid, ref respirator, false))
|
|
return;
|
|
|
|
respirator.Saturation += amount;
|
|
respirator.Saturation =
|
|
Math.Clamp(respirator.Saturation, respirator.MinSaturation, respirator.MaxSaturation);
|
|
}
|
|
|
|
private void OnApplyMetabolicMultiplier(EntityUid uid, RespiratorComponent component, ApplyMetabolicMultiplierEvent args)
|
|
{
|
|
if (args.Apply)
|
|
{
|
|
component.CycleDelay *= args.Multiplier;
|
|
component.Saturation *= args.Multiplier;
|
|
component.MaxSaturation *= args.Multiplier;
|
|
component.MinSaturation *= args.Multiplier;
|
|
return;
|
|
}
|
|
// This way we don't have to worry about it breaking if the stasis bed component is destroyed
|
|
component.CycleDelay /= args.Multiplier;
|
|
component.Saturation /= args.Multiplier;
|
|
component.MaxSaturation /= args.Multiplier;
|
|
component.MinSaturation /= args.Multiplier;
|
|
// Reset the accumulator properly
|
|
if (component.AccumulatedFrametime >= component.CycleDelay)
|
|
component.AccumulatedFrametime = component.CycleDelay;
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class InhaleLocationEvent : EntityEventArgs
|
|
{
|
|
public GasMixture? Gas;
|
|
}
|
|
|
|
public sealed class ExhaleLocationEvent : EntityEventArgs
|
|
{
|
|
public GasMixture? Gas;
|
|
}
|