Fix Respirator Asserts (#38911)

* Fix errors

* Cleanup CanMetabolizeInhaledAir

* Wait no don't do that

* Revert changes for real

* Fix

* Code cleanup and some safety rails

* Better tests and also comments

* Better comments

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
This commit is contained in:
Princess Cheeseballs
2025-07-17 09:46:38 -07:00
committed by GitHub
parent 2715933a46
commit 535646aefb
3 changed files with 143 additions and 81 deletions

View File

@@ -10,16 +10,6 @@ namespace Content.Server.Body.Components
[RegisterComponent, Access(typeof(RespiratorSystem)), AutoGenerateComponentPause]
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>

View File

@@ -48,7 +48,7 @@ public sealed class InternalsSystem : SharedInternalsSystem
return;
// Could the entity metabolise the air in the linked gas tank?
if (!_respirator.CanMetabolizeGas(uid, tank.Value.Comp.Air))
if (!_respirator.CanMetabolizeInhaledAir(uid, tank.Value.Comp.Air))
return;
ToggleInternals(uid, uid, force: false, component, ToggleMode.On);

View File

@@ -49,8 +49,13 @@ public sealed class RespiratorSystem : EntitySystem
UpdatesAfter.Add(typeof(MetabolizerSystem));
SubscribeLocalEvent<RespiratorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
// BodyComp stuff
SubscribeLocalEvent<BodyComponent, InhaledGasEvent>(OnGasInhaled);
SubscribeLocalEvent<BodyComponent, ExhaledGasEvent>(OnGasExhaled);
SubscribeLocalEvent<BodyComponent, CanMetabolizeGasEvent>(CanBodyMetabolizeGas);
SubscribeLocalEvent<BodyComponent, SuffocationEvent>(OnSuffocation);
SubscribeLocalEvent<BodyComponent, StopSuffocatingEvent>(OnStopSuffocating);
}
private void OnMapInit(Entity<RespiratorComponent> ent, ref MapInitEvent args)
@@ -80,11 +85,11 @@ public sealed class RespiratorSystem : EntitySystem
switch (respirator.Status)
{
case RespiratorStatus.Inhaling:
Inhale(uid);
Inhale((uid, respirator));
respirator.Status = RespiratorStatus.Exhaling;
break;
case RespiratorStatus.Exhaling:
Exhale(uid);
Exhale((uid, respirator));
respirator.Status = RespiratorStatus.Inhaling;
break;
}
@@ -111,10 +116,10 @@ public sealed class RespiratorSystem : EntitySystem
}
}
public bool Inhale(Entity<RespiratorComponent?> entity)
public void Inhale(Entity<RespiratorComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp, logMissing: false))
return false;
return;
// Inhale gas
var ev = new InhaleLocationEvent
@@ -126,23 +131,22 @@ public sealed class RespiratorSystem : EntitySystem
ev.Gas ??= _atmosSys.GetContainingMixture(entity.Owner, excite: true);
if (ev.Gas is null)
{
return false;
}
return;
var gas = ev.Gas.RemoveVolume(entity.Comp.BreathVolume);
var inhaleEv = new InhaledGasEvent(gas);
RaiseLocalEvent(entity, ref inhaleEv);
return inhaleEv.Handled && inhaleEv.Succeeded;
}
public void Exhale(Entity<RespiratorComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp, logMissing: false))
if (inhaleEv.Handled && inhaleEv.Succeeded)
return;
// If nothing could inhale the gas give it back.
_atmosSys.Merge(ev.Gas, gas);
}
public void Exhale(Entity<RespiratorComponent> entity)
{
// exhale gas
var ev = new ExhaleLocationEvent();
@@ -157,8 +161,16 @@ public sealed class RespiratorSystem : EntitySystem
ev.Gas ??= GasMixture.SpaceGas;
}
var exhaleEv = new ExhaledGasEvent(ev.Gas);
RaiseLocalEvent(entity, ref exhaleEv);
Exhale(entity!, ev.Gas);
}
public void Exhale(Entity<RespiratorComponent?> entity, GasMixture gas)
{
if (!Resolve(entity, ref entity.Comp, logMissing: false))
return;
var ev = new ExhaledGasEvent(gas);
RaiseLocalEvent(entity, ref ev);
}
/// <summary>
@@ -176,52 +188,76 @@ public sealed class RespiratorSystem : EntitySystem
}
/// <summary>
/// Check whether or not an entity can metabolize inhaled air without suffocating or taking damage (i.e., no toxic
/// gasses).
/// Checks if it's safe for a given entity to breathe the air from the environment it is currently situated in.
/// </summary>
/// <param name="ent">The entity attempting to metabolize the gas.</param>
/// <returns>Returns true only if the air is not toxic, and it wouldn't suffocate.</returns>
public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return false;
if (!Inhale(ent))
return false;
// Get the gas at our location but don't actually remove it from the gas mixture.
var ev = new InhaleLocationEvent
{
Respirator = ent.Comp,
};
RaiseLocalEvent(ent, ref ev);
// If we don't have a body we can't be poisoned by gas, yet...
var success = TryMetabolizeGas((ent, ent.Comp));
ev.Gas ??= _atmosSys.GetContainingMixture(ent.Owner, excite: true);
// Don't keep that gas in our lungs lest it poisons a poor nuclear operative.
Exhale(ent);
return success;
// If there's no air to breathe or we can't metabolize it then internals should be on.
return ev.Gas is not null && CanMetabolizeInhaledAir(ent, ev.Gas);
}
/// <summary>
/// Check whether or not an entity can metabolize the given gas mixture without suffocating or taking damage
/// (i.e., no toxic gasses).
/// Checks if a given entity can safely metabolize a given gas mixture.
/// </summary>
public bool CanMetabolizeGas(Entity<RespiratorComponent?> ent, GasMixture gas)
/// <param name="ent">The entity attempting to metabolize the gas.</param>
/// <param name="gas">The gas mixture we are trying to metabolize.</param>
/// <returns>Returns true only if the gas mixture is not toxic, and it wouldn't suffocate.</returns>
public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent, GasMixture gas)
{
if (!Resolve(ent, ref ent.Comp))
return false;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
if (organs.Count == 0)
var ev = new CanMetabolizeGasEvent(gas);
RaiseLocalEvent(ent, ref ev);
if (!ev.Handled || ev.Toxic)
return false;
gas = new GasMixture(gas);
var lungRatio = 1.0f / organs.Count;
gas.Multiply(MathF.Min(lungRatio * gas.Volume / ent.Comp.BreathVolume, lungRatio));
var solution = _lungSystem.GasToReagent(gas);
return ev.Saturation > ent.Comp.UpdateInterval.TotalSeconds;
}
float saturation = 0;
/// <summary>
/// Tries to safely metabolize the current solutions in a body's lungs.
/// </summary>
private void CanBodyMetabolizeGas(Entity<BodyComponent> ent, ref CanMetabolizeGasEvent args)
{
if (args.Handled)
return;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
if (organs.Count == 0)
return;
var solution = _lungSystem.GasToReagent(args.Gas);
var saturation = 0f;
foreach (var organ in organs)
{
saturation += GetSaturation(solution, organ.Owner, out var toxic);
if (toxic)
return false;
if (!toxic)
continue;
args.Handled = true;
args.Toxic = true;
return;
}
return saturation > ent.Comp.UpdateInterval.TotalSeconds;
args.Handled = true;
args.Saturation = saturation;
}
public bool TryInhaleGasToBody(Entity<BodyComponent?> entity, GasMixture gas)
@@ -265,30 +301,6 @@ public sealed class RespiratorSystem : EntitySystem
_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>
@@ -354,15 +366,11 @@ public sealed class RespiratorSystem : EntitySystem
_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
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
foreach (var entity in organs)
{
_alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
}
}
if (ent.Comp.SuffocationCycles < ent.Comp.SuffocationCycleThreshold)
return;
var ev = new SuffocationEvent();
RaiseLocalEvent(ent, ref ev);
}
private void StopSuffocation(Entity<RespiratorComponent> ent)
@@ -372,6 +380,22 @@ public sealed class RespiratorSystem : EntitySystem
_damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);
var ev = new StopSuffocatingEvent();
RaiseLocalEvent(ent, ref ev);
}
private void OnSuffocation(Entity<BodyComponent> ent, ref SuffocationEvent args)
{
// 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.ShowAlert(ent, entity.Comp1.Alert);
}
}
private void OnStopSuffocating(Entity<BodyComponent> ent, ref StopSuffocatingEvent args)
{
// 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)
@@ -397,6 +421,9 @@ public sealed class RespiratorSystem : EntitySystem
private void OnGasInhaled(Entity<BodyComponent> entity, ref InhaledGasEvent args)
{
if (args.Handled)
return;
args.Handled = true;
args.Succeeded = TryInhaleGasToBody((entity, entity.Comp), args.Gas);
@@ -404,20 +431,65 @@ public sealed class RespiratorSystem : EntitySystem
private void OnGasExhaled(Entity<BodyComponent> entity, ref ExhaledGasEvent args)
{
if (args.Handled)
return;
args.Handled = true;
RemoveGasFromBody(entity, args.Gas);
}
}
/// <summary>
/// Event raised when an entity first tries to inhale that returns a GasMixture from a given location.
/// </summary>
/// <param name="Gas">The gas that gets returned, null if there is none.</param>
/// <param name="Respirator">The Respirator component of the entity attempting to inhale</param>
[ByRefEvent]
public record struct InhaleLocationEvent(GasMixture? Gas, RespiratorComponent Respirator);
/// <summary>
/// Event raised when an entity first tries to exhale a gas, determines where the gas they're exhaling will be sent.
/// </summary>
/// <param name="Gas">The gas mixture that the exhaled gas will be merged into.</param>
[ByRefEvent]
public record struct ExhaleLocationEvent(GasMixture? Gas);
/// <summary>
/// Event raised when an entity successfully inhales a gas, attempts to find a place to put the gas.
/// </summary>
/// <param name="Gas">The gas we're inhaling.</param>
/// <param name="Handled">Whether a system has responded appropriately.</param>
/// <param name="Succeeded">Whether we successfully managed to inhale the gas</param>
[ByRefEvent]
public record struct InhaledGasEvent(GasMixture Gas, bool Handled = false, bool Succeeded = false);
/// <summary>
/// Event raised when an entity is exhaling
/// </summary>
/// <param name="Gas">The gas mixture we're exhaling into.</param>
/// <param name="Handled">Whether we have successfully exhaled or not.</param>
[ByRefEvent]
public record struct ExhaledGasEvent(GasMixture Gas, bool Handled = false);
/// <summary>
/// Raised when an entity starts suffocating and when suffocation progresses.
/// </summary>
[ByRefEvent]
public record struct SuffocationEvent;
/// <summary>
/// Raised when an entity that was suffocating stops suffocating.
/// </summary>
[ByRefEvent]
public record struct StopSuffocatingEvent;
/// <summary>
/// An event raised to inhalation handlers that asks them nicely to simulate what it would be like to metabolize
/// a given volume of gas, without actually metabolizing it.
/// </summary>
/// <param name="Gas">The gas mixture we are testing.</param>
/// <param name="Toxic">Whether the gas returns as toxic to any respirator.</param>
/// <param name="Saturation">The amount of saturation we got from the gas.</param>
[ByRefEvent]
public record struct CanMetabolizeGasEvent(GasMixture Gas, bool Toxic = false, float Saturation = 0f, bool Handled = false);