if (tile?.Air == null)
continue;
- tile.Air.CopyFromMutable(combined);
+ tile.Air.CopyFrom(combined);
InvalidateVisuals(ent, tile);
}
[Dependency] private readonly GasTankSystem _gasTank = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly RespiratorSystem _respirator = default!;
private EntityQuery<InternalsComponent> _internalsQuery;
SubscribeLocalEvent<InternalsComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs);
SubscribeLocalEvent<InternalsComponent, InternalsDoAfterEvent>(OnDoAfter);
- SubscribeLocalEvent<StartingGearEquippedEvent>(OnStartingGear);
+ SubscribeLocalEvent<InternalsComponent, StartingGearEquippedEvent>(OnStartingGear);
}
- private void OnStartingGear(ref StartingGearEquippedEvent ev)
+ private void OnStartingGear(EntityUid uid, InternalsComponent component, ref StartingGearEquippedEvent args)
{
- if (!_internalsQuery.TryComp(ev.Entity, out var internals) || internals.BreathToolEntity == null)
+ if (component.BreathToolEntity == null)
return;
- ToggleInternals(ev.Entity, ev.Entity, force: false, internals);
+ if (component.GasTankEntity != null)
+ return; // already connected
+
+ // Can the entity breathe the air it is currently exposed to?
+ if (_respirator.CanMetabolizeInhaledAir(uid))
+ return;
+
+ var tank = FindBestGasTank(uid);
+ if (tank == null)
+ return;
+
+ // Could the entity metabolise the air in the linked gas tank?
+ if (!_respirator.CanMetabolizeGas(uid, tank.Value.Comp.Air))
+ return;
+
+ ToggleInternals(uid, uid, force: false, component);
}
private void OnGetInteractionVerbs(
public Entity<GasTankComponent>? FindBestGasTank(
Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user)
{
+ // TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses
// Prioritise
// 1. back equipped tanks
// 2. exo-slot tanks
using Content.Server.Body.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Atmos;
+using Content.Shared.Chemistry.Components;
using Content.Shared.Clothing;
using Content.Shared.Inventory.Events;
if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution))
return;
- foreach (var gas in Enum.GetValues<Gas>())
+ GasToReagent(lung.Air, solution);
+ _solutionContainerSystem.UpdateChemicals(lung.Solution.Value);
+ }
+
+ private void GasToReagent(GasMixture gas, Solution solution)
+ {
+ foreach (var gasId in Enum.GetValues<Gas>())
{
- var i = (int) gas;
- var moles = lung.Air[i];
+ var i = (int) gasId;
+ var moles = gas[i];
if (moles <= 0)
continue;
+
var reagent = _atmosphereSystem.GasReagents[i];
- if (reagent is null) continue;
+ if (reagent is null)
+ continue;
var amount = moles * Atmospherics.BreathMolesToReagentMultiplier;
solution.AddReagent(reagent, amount);
-
- // We don't remove the gas from the lung mix,
- // that's the responsibility of whatever gas is being metabolized.
- // Most things will just want to exhale again.
}
+ }
- _solutionContainerSystem.UpdateChemicals(lung.Solution.Value);
+ public Solution GasToReagent(GasMixture gas)
+ {
+ var solution = new Solution();
+ GasToReagent(gas, solution);
+ return solution;
}
}
using Content.Server.Administration.Logs;
-using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Chat.Systems;
using Content.Server.Chemistry.Containers.EntitySystems;
+using Content.Server.Chemistry.ReagentEffectConditions;
+using Content.Server.Chemistry.ReagentEffects;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Body.Components;
+using Content.Shared.Body.Prototypes;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Mobs.Systems;
using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.Body.Systems;
[Dependency] private readonly DamageableSystem _damageableSys = default!;
[Dependency] private readonly LungSystem _lungSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly ChatSystem _chat = default!;
+ private static readonly ProtoId<MetabolismGroupPrototype> GasId = new("Gas");
+
public override void Initialize()
{
base.Initialize();
// Inhale gas
var ev = new InhaleLocationEvent();
- RaiseLocalEvent(uid, ref ev, broadcast: false);
+ RaiseLocalEvent(uid, ref ev);
ev.Gas ??= _atmosSys.GetContainingMixture(uid, excite: true);
_atmosSys.Merge(ev.Gas, outGas);
}
+ /// <summary>
+ /// Check whether or not an entity can metabolize inhaled air without suffocating or taking damage (i.e., no toxic
+ /// gasses).
+ /// </summary>
+ public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent)
+ {
+ 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)
+ return false;
+
+ return CanMetabolizeGas(ent, gas);
+ }
+
+ /// <summary>
+ /// Check whether or not an entity can metabolize the given gas mixture without suffocating or taking damage
+ /// (i.e., no toxic gasses).
+ /// </summary>
+ public bool CanMetabolizeGas(Entity<RespiratorComponent?> ent, GasMixture gas)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return false;
+
+ var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(ent);
+ if (organs.Count == 0)
+ return false;
+
+ gas = new GasMixture(gas);
+ var lungRatio = 1.0f / organs.Count;
+ gas.Multiply(MathF.Min(lungRatio * gas.Volume/Atmospherics.BreathVolume, lungRatio));
+ var solution = _lungSystem.GasToReagent(gas);
+
+ float saturation = 0;
+ foreach (var organ in organs)
+ {
+ saturation += GetSaturation(solution, organ.Comp.Owner, out var toxic);
+ if (toxic)
+ return false;
+ }
+
+ return saturation > ent.Comp.UpdateInterval.TotalSeconds;
+ }
+
+ /// <summary>
+ /// Get the amount of saturation that would be generated if the lung were to metabolize the given solution.
+ /// </summary>
+ /// <remarks>
+ /// This assumes the metabolism rate is unbounded, which generally should be the case for lungs, otherwise we get
+ /// back to the old pulmonary edema bug.
+ /// </remarks>
+ /// <param name="solution">The reagents to metabolize</param>
+ /// <param name="lung">The entity doing the metabolizing</param>
+ /// <param name="toxic">Whether or not any of the reagents would deal damage to the entity</param>
+ private float GetSaturation(Solution solution, Entity<MetabolizerComponent?> lung, out bool toxic)
+ {
+ toxic = false;
+ if (!Resolve(lung, ref lung.Comp))
+ return 0;
+
+ if (lung.Comp.MetabolismGroups == null)
+ return 0;
+
+ float saturation = 0;
+ foreach (var (id, quantity) in solution.Contents)
+ {
+ var reagent = _protoMan.Index<ReagentPrototype>(id.Prototype);
+ if (reagent.Metabolisms == null)
+ continue;
+
+ if (!reagent.Metabolisms.TryGetValue(GasId, out var entry))
+ continue;
+
+ foreach (var effect in entry.Effects)
+ {
+ if (effect is HealthChange health)
+ toxic |= CanMetabolize(health) && health.Damage.AnyPositive();
+ else if (effect is Oxygenate oxy && CanMetabolize(oxy))
+ saturation += oxy.Factor * quantity.Float();
+ }
+ }
+
+ // TODO generalize condition checks
+ // this is pretty janky, but I just want to bodge a method that checks if an entity can breathe a gas mixture
+ // Applying actual reaction effects require a full ReagentEffectArgs struct.
+ bool CanMetabolize(ReagentEffect effect)
+ {
+ if (effect.Conditions == null)
+ return true;
+
+ foreach (var cond in effect.Conditions)
+ {
+ if (cond is OrganType organ && !organ.Condition(lung, EntityManager))
+ return false;
+ }
+
+ return true;
+ }
+
+ return saturation;
+ }
+
private void TakeSuffocationDamage(Entity<RespiratorComponent> ent)
{
if (ent.Comp.SuffocationCycles == 2)
if (args.OrganEntity == null)
return false;
- if (args.EntityManager.TryGetComponent<MetabolizerComponent>(args.OrganEntity.Value, out var metabolizer)
- && metabolizer.MetabolizerTypes != null
- && metabolizer.MetabolizerTypes.Contains(Type))
+ return Condition(args.OrganEntity.Value, args.EntityManager);
+ }
+
+ public bool Condition(Entity<MetabolizerComponent?> metabolizer, IEntityManager entMan)
+ {
+ metabolizer.Comp ??= entMan.GetComponentOrNull<MetabolizerComponent>(metabolizer.Owner);
+ if (metabolizer.Comp != null
+ && metabolizer.Comp.MetabolizerTypes != null
+ && metabolizer.Comp.MetabolizerTypes.Contains(Type))
return ShouldHave;
return !ShouldHave;
}
private void OnAttemptPacifiedThrow(Entity<DamageOnLandComponent> ent, ref AttemptPacifiedThrowEvent args)
{
// Allow healing projectiles, forbid any that do damage:
- if (ent.Comp.Damage.Any())
+ if (ent.Comp.Damage.AnyPositive())
{
args.Cancel("pacified-cannot-throw");
}
if (!electrified.OnAttacked)
return;
- if (!_meleeWeapon.GetDamage(args.Used, args.User).Any())
+ if (_meleeWeapon.GetDamage(args.Used, args.User).Empty)
return;
TryDoElectrifiedAct(uid, args.User, 1, electrified);
if (!component.CurrentLit || args.Used != args.User)
return;
- if (!_meleeWeapon.GetDamage(args.Used, args.User).Any())
+ if (_meleeWeapon.GetDamage(args.Used, args.User).Empty)
return;
DoCommonElectrocution(args.User, uid, component.UnarmedHitShock, component.UnarmedHitStun, false);
// don't raise BeforeExplodeEvent if the entity is completely immune to explosions
var thisDamage = GetDamage(uid, prototype, originalDamage);
- if (!thisDamage.Any())
+ if (thisDamage.Empty)
return;
_toDamage.Add((uid, thisDamage));
if (modifiedDamage is not null && EntityManager.EntityExists(component.Shooter))
{
- if (modifiedDamage.Any() && !deleted)
+ if (modifiedDamage.AnyPositive() && !deleted)
{
_color.RaiseEffect(Color.Red, new List<EntityUid> { target }, Filter.Pvs(target, entityManager: EntityManager));
}
}
var gearEquippedEv = new StartingGearEquippedEvent(entity.Value);
- RaiseLocalEvent(entity.Value, ref gearEquippedEv, true);
+ RaiseLocalEvent(entity.Value, ref gearEquippedEv);
if (profile != null)
{
// Move contained air
if (component.BehaviorProperties.TransportGas)
{
- entityStorageComponent.Air.CopyFromMutable(target.Value.storageComponent.Air);
+ entityStorageComponent.Air.CopyFrom(target.Value.storageComponent.Air);
target.Value.storageComponent.Air.Clear();
}
// Move contained air
if (component.BehaviorProperties.TransportGas)
{
- target.Value.storageComponent.Air.CopyFromMutable(entityStorageComponent.Air);
+ target.Value.storageComponent.Air.CopyFrom(entityStorageComponent.Air);
entityStorageComponent.Air.Clear();
}
{
if (!Deleted(hitEntity))
{
- if (dmg.Any())
+ if (dmg.AnyPositive())
{
_color.RaiseEffect(Color.Red, new List<EntityUid>() { hitEntity }, Filter.Pvs(hitEntity, entityManager: EntityManager));
}
Volume = volume;
}
+ public GasMixture(GasMixture toClone)
+ {
+ CopyFrom(toClone);
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkImmutable()
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void CopyFromMutable(GasMixture sample)
+ public void CopyFrom(GasMixture sample)
{
- if (Immutable) return;
+ if (Immutable)
+ return;
+
+ Volume = sample.Volume;
sample.Moles.CopyTo(Moles, 0);
Temperature = sample.Temperature;
}
/// </summary>
/// <remarks>
/// Note that this being zero does not mean this damage has no effect. Healing in one type may cancel damage
- /// in another. Consider using <see cref="Any()"/> or <see cref="Empty"/> instead.
+ /// in another. Consider using <see cref="AnyPositive"/> or <see cref="Empty"/> instead.
/// </remarks>
public FixedPoint2 GetTotal()
{
/// Differs from <see cref="Empty"/> as a damage specifier might contain entries with zeroes.
/// This also returns false if the specifier only contains negative values.
/// </summary>
- public bool Any()
+ public bool AnyPositive()
{
foreach (var value in DamageDict.Values)
{
if (raiseEvent)
{
var ev = new StartingGearEquippedEvent(entity);
- RaiseLocalEvent(entity, ref ev, true);
+ RaiseLocalEvent(entity, ref ev);
}
}
}
var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin:user);
- if (damageResult != null && damageResult.Any())
+ if (damageResult is {Empty: false})
{
// If the target has stamina and is taking blunt damage, they should also take stamina damage based on their blunt to stamina factor
if (damageResult.DamageDict.TryGetValue("Blunt", out var bluntDamage))