RespiratorSystem Cleanup (#38572)

* Respirator Debodied

* Forgot about alerts (also respirator testa and events)

* Fix Urist eating air and not giving it back

* Stop nuke ops from taking in a breath then taking in a second breath causing them to get a headache from carbon dioxide poisoning and failing TryStopNukeOpsFromConstantlyFailing();

* Consts are smelly,

* Actually we don't need to raise the entity, just the component

* Don't forget to remove the unused code today, said me yesterday

* Remove all fallbacks

* Debody that too

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
This commit is contained in:
Princess Cheeseballs
2025-06-26 19:33:43 -07:00
committed by GitHub
parent e916135a9c
commit d7d83bd87c
4 changed files with 158 additions and 54 deletions

View File

@@ -1,4 +1,6 @@
using Content.Server.Body.Systems;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Damage;
using Robust.Shared.Prototypes;
@@ -9,6 +11,28 @@ namespace Content.Server.Body.Components
[RegisterComponent, Access(typeof(RespiratorSystem))]
public sealed partial class RespiratorComponent : Component
{
/// <summary>
/// Gas container for this entity
/// </summary>
[DataField]
public GasMixture Air = new()
{
Volume = 6, // 6 liters, the average lung capacity for a human according to Google
Temperature = Atmospherics.NormalBodyTemperature
};
/// <summary>
/// Volume of our breath in liters
/// </summary>
[DataField]
public float BreathVolume = Atmospherics.BreathVolume;
/// <summary>
/// How much of the gas we inhale is metabolized? Value range is (0, 1]
/// </summary>
[DataField]
public float Ratio = 1.0f;
/// <summary>
/// The next time that this body will inhale or exhale.
/// </summary>

View File

@@ -1,4 +1,5 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Popups;
using Content.Shared.Alert;
using Content.Shared.Atmos;
@@ -58,7 +59,7 @@ public sealed class InternalsSystem : SharedInternalsSystem
if (AreInternalsWorking(ent))
{
var gasTank = Comp<GasTankComponent>(ent.Comp.GasTankEntity!.Value);
args.Gas = _gasTank.RemoveAirVolume((ent.Comp.GasTankEntity.Value, gasTank), Atmospherics.BreathVolume);
args.Gas = _gasTank.RemoveAirVolume((ent.Comp.GasTankEntity.Value, gasTank), args.Respirator.BreathVolume);
// TODO: Should listen to gas tank updates instead I guess?
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
}

View File

@@ -63,6 +63,9 @@ public sealed class LungSystem : EntitySystem
_solutionContainerSystem.UpdateChemicals(lung.Solution.Value);
}
/* This should really be moved to somewhere in the atmos system and modernized,
so that other systems, like CondenserSystem, can use it.
*/
private void GasToReagent(GasMixture gas, Solution solution)
{
foreach (var gasId in Enum.GetValues<Gas>())

View File

@@ -50,6 +50,8 @@ public sealed class RespiratorSystem : EntitySystem
SubscribeLocalEvent<RespiratorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RespiratorComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
SubscribeLocalEvent<BodyComponent, InhaledGasEvent>(OnGasInhaled);
SubscribeLocalEvent<BodyComponent, ExhaledGasEvent>(OnGasExhaled);
}
private void OnMapInit(Entity<RespiratorComponent> ent, ref MapInitEvent args)
@@ -84,11 +86,11 @@ public sealed class RespiratorSystem : EntitySystem
switch (respirator.Status)
{
case RespiratorStatus.Inhaling:
Inhale(uid, body);
Inhale(uid);
respirator.Status = RespiratorStatus.Exhaling;
break;
case RespiratorStatus.Exhaling:
Exhale(uid, body);
Exhale(uid);
respirator.Status = RespiratorStatus.Inhaling;
break;
}
@@ -99,7 +101,10 @@ public sealed class RespiratorSystem : EntitySystem
if (_gameTiming.CurTime >= respirator.LastGaspEmoteTime + respirator.GaspEmoteCooldown)
{
respirator.LastGaspEmoteTime = _gameTiming.CurTime;
_chat.TryEmoteWithChat(uid, respirator.GaspEmote, ChatTransmitRange.HideChat, ignoreActionBlocker: true);
_chat.TryEmoteWithChat(uid,
respirator.GaspEmote,
ChatTransmitRange.HideChat,
ignoreActionBlocker: true);
}
TakeSuffocationDamage((uid, respirator));
@@ -112,68 +117,54 @@ public sealed class RespiratorSystem : EntitySystem
}
}
public void Inhale(EntityUid uid, BodyComponent? body = null)
public bool Inhale(Entity<RespiratorComponent?> entity)
{
if (!Resolve(uid, ref body, logMissing: false))
return;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((uid, body));
if (!Resolve(entity, ref entity.Comp, logMissing: false))
return false;
// Inhale gas
var ev = new InhaleLocationEvent();
RaiseLocalEvent(uid, ref ev);
var ev = new InhaleLocationEvent
{
Respirator = entity.Comp,
};
RaiseLocalEvent(entity, ref ev);
ev.Gas ??= _atmosSys.GetContainingMixture(uid, excite: true);
ev.Gas ??= _atmosSys.GetContainingMixture(entity.Owner, excite: true);
if (ev.Gas is null)
{
return;
return false;
}
var actualGas = ev.Gas.RemoveVolume(Atmospherics.BreathVolume);
var gas = ev.Gas.RemoveVolume(entity.Comp.BreathVolume);
var lungRatio = 1.0f / organs.Count;
var gas = organs.Count == 1 ? actualGas : actualGas.RemoveRatio(lungRatio);
foreach (var (organUid, lung, _) in organs)
var inhaleEv = new InhaledGasEvent(gas);
RaiseLocalEvent(entity, ref inhaleEv);
return inhaleEv.Handled && inhaleEv.Succeeded;
}
public void Exhale(Entity<RespiratorComponent?> entity)
{
// Merge doesn't remove gas from the giver.
_atmosSys.Merge(lung.Air, gas);
_lungSystem.GasToReagent(organUid, lung);
}
}
public void Exhale(EntityUid uid, BodyComponent? body = null)
{
if (!Resolve(uid, ref body, logMissing: false))
if (!Resolve(entity, ref entity.Comp, logMissing: false))
return;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((uid, body));
// exhale gas
var ev = new ExhaleLocationEvent();
RaiseLocalEvent(uid, ref ev, broadcast: false);
RaiseLocalEvent(entity, ref ev, broadcast: false);
if (ev.Gas is null)
{
ev.Gas = _atmosSys.GetContainingMixture(uid, excite: true);
ev.Gas = _atmosSys.GetContainingMixture(entity.Owner, excite: 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 (organUid, lung, _) in organs)
{
_atmosSys.Merge(outGas, lung.Air);
lung.Air.Clear();
if (_solutionContainerSystem.ResolveSolution(organUid, lung.SolutionName, ref lung.Solution))
_solutionContainerSystem.RemoveAllSolution(lung.Solution.Value);
}
_atmosSys.Merge(ev.Gas, outGas);
var exhaleEv = new ExhaledGasEvent(ev.Gas);
RaiseLocalEvent(entity, ref exhaleEv);
}
/// <summary>
@@ -199,14 +190,15 @@ public sealed class RespiratorSystem : EntitySystem
if (!Resolve(ent, ref ent.Comp))
return false;
var ev = new InhaleLocationEvent();
RaiseLocalEvent(ent, ref ev);
var gas = ev.Gas ?? _atmosSys.GetContainingMixture(ent.Owner);
if (gas == null)
if (!Inhale(ent))
return false;
return CanMetabolizeGas(ent, gas);
// If we don't have a body we can't be poisoned by gas, yet...
var success = TryMetabolizeGas((ent, ent.Comp));
// Don't keep that gas in our lungs lest it poisons a poor nuclear operative.
Exhale(ent);
return success;
}
/// <summary>
@@ -224,7 +216,7 @@ public sealed class RespiratorSystem : EntitySystem
gas = new GasMixture(gas);
var lungRatio = 1.0f / organs.Count;
gas.Multiply(MathF.Min(lungRatio * gas.Volume/Atmospherics.BreathVolume, lungRatio));
gas.Multiply(MathF.Min(lungRatio * gas.Volume / ent.Comp.BreathVolume, lungRatio));
var solution = _lungSystem.GasToReagent(gas);
float saturation = 0;
@@ -238,6 +230,71 @@ public sealed class RespiratorSystem : EntitySystem
return saturation > ent.Comp.UpdateInterval.TotalSeconds;
}
public bool TryInhaleGasToBody(Entity<BodyComponent?> entity, GasMixture gas)
{
if (!Resolve(entity, ref entity.Comp))
return false;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((entity, entity.Comp));
if (organs.Count == 0)
return false;
var lungRatio = 1.0f / organs.Count;
var splitGas = organs.Count == 1 ? gas : gas.RemoveRatio(lungRatio);
foreach (var (organUid, lung, _) in organs)
{
// Merge doesn't remove gas from the giver.
_atmosSys.Merge(lung.Air, splitGas);
_lungSystem.GasToReagent(organUid, lung);
}
return true;
}
public void RemoveGasFromBody(Entity<BodyComponent> ent, GasMixture gas)
{
var outGas = new GasMixture(gas.Volume);
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, ent.Comp));
if (organs.Count == 0)
return;
foreach (var (organUid, lung, _) in organs)
{
_atmosSys.Merge(outGas, lung.Air);
lung.Air.Clear();
if (_solutionContainerSystem.ResolveSolution(organUid, lung.SolutionName, ref lung.Solution))
_solutionContainerSystem.RemoveAllSolution(lung.Solution.Value);
}
_atmosSys.Merge(gas, outGas);
}
/// <summary>
/// Tries to safely metabolize the current solutions in a body's lungs.
/// </summary>
private bool TryMetabolizeGas(Entity<RespiratorComponent, BodyComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp2))
return false;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
if (organs.Count == 0)
return false;
float saturation = 0;
foreach (var organ in organs)
{
var solution = _lungSystem.GasToReagent(organ.Comp1.Air);
saturation += GetSaturation(solution, organ.Owner, out var toxic);
if (toxic)
return false;
}
return saturation > ent.Comp1.UpdateInterval.TotalSeconds;
}
/// <summary>
/// Get the amount of saturation that would be generated if the lung were to metabolize the given solution.
/// </summary>
@@ -301,6 +358,8 @@ public sealed class RespiratorSystem : EntitySystem
if (ent.Comp.SuffocationCycles == 2)
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} started suffocating");
_damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false);
if (ent.Comp.SuffocationCycles >= ent.Comp.SuffocationCycleThreshold)
{
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
@@ -310,8 +369,6 @@ public sealed class RespiratorSystem : EntitySystem
_alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
}
}
_damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false);
}
private void StopSuffocation(Entity<RespiratorComponent> ent)
@@ -319,18 +376,17 @@ public sealed class RespiratorSystem : EntitySystem
if (ent.Comp.SuffocationCycles >= 2)
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} stopped suffocating");
_damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
foreach (var entity in organs)
{
_alertsSystem.ClearAlert(ent, entity.Comp1.Alert);
}
_damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);
}
public void UpdateSaturation(EntityUid uid, float amount,
RespiratorComponent? respirator = null)
public void UpdateSaturation(EntityUid uid, float amount, RespiratorComponent? respirator = null)
{
if (!Resolve(uid, ref respirator, false))
return;
@@ -362,10 +418,30 @@ public sealed class RespiratorSystem : EntitySystem
ent.Comp.MaxSaturation /= args.Multiplier;
ent.Comp.MinSaturation /= args.Multiplier;
}
private void OnGasInhaled(Entity<BodyComponent> entity, ref InhaledGasEvent args)
{
args.Handled = true;
args.Succeeded = TryInhaleGasToBody((entity, entity.Comp), args.Gas);
}
private void OnGasExhaled(Entity<BodyComponent> entity, ref ExhaledGasEvent args)
{
args.Handled = true;
RemoveGasFromBody(entity, args.Gas);
}
}
[ByRefEvent]
public record struct InhaleLocationEvent(GasMixture? Gas);
public record struct InhaleLocationEvent(GasMixture? Gas, RespiratorComponent Respirator);
[ByRefEvent]
public record struct ExhaleLocationEvent(GasMixture? Gas);
[ByRefEvent]
public record struct InhaledGasEvent(GasMixture Gas, bool Handled = false, bool Succeeded = false);
[ByRefEvent]
public record struct ExhaledGasEvent(GasMixture Gas, bool Handled = false);