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.Server.Body.Systems;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Chat.Prototypes; using Content.Shared.Chat.Prototypes;
using Content.Shared.Damage; using Content.Shared.Damage;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -9,6 +11,28 @@ namespace Content.Server.Body.Components
[RegisterComponent, Access(typeof(RespiratorSystem))] [RegisterComponent, Access(typeof(RespiratorSystem))]
public sealed partial class RespiratorComponent : Component 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> /// <summary>
/// The next time that this body will inhale or exhale. /// The next time that this body will inhale or exhale.
/// </summary> /// </summary>

View File

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

View File

@@ -63,6 +63,9 @@ public sealed class LungSystem : EntitySystem
_solutionContainerSystem.UpdateChemicals(lung.Solution.Value); _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) private void GasToReagent(GasMixture gas, Solution solution)
{ {
foreach (var gasId in Enum.GetValues<Gas>()) foreach (var gasId in Enum.GetValues<Gas>())

View File

@@ -50,6 +50,8 @@ public sealed class RespiratorSystem : EntitySystem
SubscribeLocalEvent<RespiratorComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<RespiratorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RespiratorComponent, EntityUnpausedEvent>(OnUnpaused); SubscribeLocalEvent<RespiratorComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier); SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
SubscribeLocalEvent<BodyComponent, InhaledGasEvent>(OnGasInhaled);
SubscribeLocalEvent<BodyComponent, ExhaledGasEvent>(OnGasExhaled);
} }
private void OnMapInit(Entity<RespiratorComponent> ent, ref MapInitEvent args) private void OnMapInit(Entity<RespiratorComponent> ent, ref MapInitEvent args)
@@ -77,18 +79,18 @@ public sealed class RespiratorSystem : EntitySystem
if (_mobState.IsDead(uid)) if (_mobState.IsDead(uid))
continue; continue;
UpdateSaturation(uid, -(float) respirator.UpdateInterval.TotalSeconds, respirator); UpdateSaturation(uid, -(float)respirator.UpdateInterval.TotalSeconds, respirator);
if (!_mobState.IsIncapacitated(uid)) // cannot breathe in crit. if (!_mobState.IsIncapacitated(uid)) // cannot breathe in crit.
{ {
switch (respirator.Status) switch (respirator.Status)
{ {
case RespiratorStatus.Inhaling: case RespiratorStatus.Inhaling:
Inhale(uid, body); Inhale(uid);
respirator.Status = RespiratorStatus.Exhaling; respirator.Status = RespiratorStatus.Exhaling;
break; break;
case RespiratorStatus.Exhaling: case RespiratorStatus.Exhaling:
Exhale(uid, body); Exhale(uid);
respirator.Status = RespiratorStatus.Inhaling; respirator.Status = RespiratorStatus.Inhaling;
break; break;
} }
@@ -99,7 +101,10 @@ public sealed class RespiratorSystem : EntitySystem
if (_gameTiming.CurTime >= respirator.LastGaspEmoteTime + respirator.GaspEmoteCooldown) if (_gameTiming.CurTime >= respirator.LastGaspEmoteTime + respirator.GaspEmoteCooldown)
{ {
respirator.LastGaspEmoteTime = _gameTiming.CurTime; 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)); 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)) if (!Resolve(entity, ref entity.Comp, logMissing: false))
return; return false;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((uid, body));
// Inhale gas // Inhale gas
var ev = new InhaleLocationEvent(); var ev = new InhaleLocationEvent
RaiseLocalEvent(uid, ref ev); {
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) 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 inhaleEv = new InhaledGasEvent(gas);
var gas = organs.Count == 1 ? actualGas : actualGas.RemoveRatio(lungRatio); RaiseLocalEvent(entity, ref inhaleEv);
foreach (var (organUid, lung, _) in organs)
{ return inhaleEv.Handled && inhaleEv.Succeeded;
// 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) public void Exhale(Entity<RespiratorComponent?> entity)
{ {
if (!Resolve(uid, ref body, logMissing: false)) if (!Resolve(entity, ref entity.Comp, logMissing: false))
return; return;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((uid, body));
// exhale gas // exhale gas
var ev = new ExhaleLocationEvent(); var ev = new ExhaleLocationEvent();
RaiseLocalEvent(uid, ref ev, broadcast: false); RaiseLocalEvent(entity, ref ev, broadcast: false);
if (ev.Gas is null) 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, // 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. // but this also means you cannot exhale on some grids.
ev.Gas ??= GasMixture.SpaceGas; ev.Gas ??= GasMixture.SpaceGas;
} }
var outGas = new GasMixture(ev.Gas.Volume); var exhaleEv = new ExhaledGasEvent(ev.Gas);
foreach (var (organUid, lung, _) in organs) RaiseLocalEvent(entity, ref exhaleEv);
{
_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);
} }
/// <summary> /// <summary>
@@ -199,14 +190,15 @@ public sealed class RespiratorSystem : EntitySystem
if (!Resolve(ent, ref ent.Comp)) if (!Resolve(ent, ref ent.Comp))
return false; return false;
var ev = new InhaleLocationEvent(); if (!Inhale(ent))
RaiseLocalEvent(ent, ref ev);
var gas = ev.Gas ?? _atmosSys.GetContainingMixture(ent.Owner);
if (gas == null)
return false; 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> /// <summary>
@@ -224,7 +216,7 @@ public sealed class RespiratorSystem : EntitySystem
gas = new GasMixture(gas); gas = new GasMixture(gas);
var lungRatio = 1.0f / organs.Count; 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); var solution = _lungSystem.GasToReagent(gas);
float saturation = 0; float saturation = 0;
@@ -238,6 +230,71 @@ public sealed class RespiratorSystem : EntitySystem
return saturation > ent.Comp.UpdateInterval.TotalSeconds; 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> /// <summary>
/// Get the amount of saturation that would be generated if the lung were to metabolize the given solution. /// Get the amount of saturation that would be generated if the lung were to metabolize the given solution.
/// </summary> /// </summary>
@@ -301,6 +358,8 @@ public sealed class RespiratorSystem : EntitySystem
if (ent.Comp.SuffocationCycles == 2) if (ent.Comp.SuffocationCycles == 2)
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} started suffocating"); _adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} started suffocating");
_damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false);
if (ent.Comp.SuffocationCycles >= ent.Comp.SuffocationCycleThreshold) if (ent.Comp.SuffocationCycles >= ent.Comp.SuffocationCycleThreshold)
{ {
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility // 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); _alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
} }
} }
_damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false);
} }
private void StopSuffocation(Entity<RespiratorComponent> ent) private void StopSuffocation(Entity<RespiratorComponent> ent)
@@ -319,18 +376,17 @@ public sealed class RespiratorSystem : EntitySystem
if (ent.Comp.SuffocationCycles >= 2) if (ent.Comp.SuffocationCycles >= 2)
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} stopped suffocating"); _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 // TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null)); var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
foreach (var entity in organs) foreach (var entity in organs)
{ {
_alertsSystem.ClearAlert(ent, entity.Comp1.Alert); _alertsSystem.ClearAlert(ent, entity.Comp1.Alert);
} }
_damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);
} }
public void UpdateSaturation(EntityUid uid, float amount, public void UpdateSaturation(EntityUid uid, float amount, RespiratorComponent? respirator = null)
RespiratorComponent? respirator = null)
{ {
if (!Resolve(uid, ref respirator, false)) if (!Resolve(uid, ref respirator, false))
return; return;
@@ -362,10 +418,30 @@ public sealed class RespiratorSystem : EntitySystem
ent.Comp.MaxSaturation /= args.Multiplier; ent.Comp.MaxSaturation /= args.Multiplier;
ent.Comp.MinSaturation /= 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] [ByRefEvent]
public record struct InhaleLocationEvent(GasMixture? Gas); public record struct InhaleLocationEvent(GasMixture? Gas, RespiratorComponent Respirator);
[ByRefEvent] [ByRefEvent]
public record struct ExhaleLocationEvent(GasMixture? Gas); 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);