From 54337911d3995075caf4bbc6b6d768a93e8c1d0a Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Fri, 31 May 2024 14:28:11 +1200 Subject: [PATCH] Only auto-enable internals when necessary (#28248) * Only auto-enable internals when necessary * Add toxic gas check * Rename Any -> AnyPositive --- .../AtmosphereSystem.ExcitedGroup.cs | 2 +- .../Body/Systems/InternalsSystem.cs | 25 +++- Content.Server/Body/Systems/LungSystem.cs | 28 +++-- .../Body/Systems/RespiratorSystem.cs | 118 +++++++++++++++++- .../ReagentEffectConditions/OrganType.cs | 12 +- .../Damage/Systems/DamageOnLandSystem.cs | 2 +- .../Electrocution/ElectrocutionSystem.cs | 4 +- .../ExplosionSystem.Processing.cs | 2 +- .../Projectiles/ProjectileSystem.cs | 2 +- .../Station/Systems/StationSpawningSystem.cs | 2 +- .../EntitySystems/BluespaceLockerSystem.cs | 4 +- .../Weapons/Ranged/Systems/GunSystem.cs | 2 +- Content.Shared/Atmos/GasMixture.cs | 12 +- Content.Shared/Damage/DamageSpecifier.cs | 4 +- .../Station/SharedStationSpawningSystem.cs | 2 +- .../Weapons/Melee/SharedMeleeWeaponSystem.cs | 2 +- 16 files changed, 189 insertions(+), 34 deletions(-) diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs index c1b58f7a77..0d622f3067 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs @@ -106,7 +106,7 @@ namespace Content.Server.Atmos.EntitySystems if (tile?.Air == null) continue; - tile.Air.CopyFromMutable(combined); + tile.Air.CopyFrom(combined); InvalidateVisuals(ent, tile); } diff --git a/Content.Server/Body/Systems/InternalsSystem.cs b/Content.Server/Body/Systems/InternalsSystem.cs index c1e1de2baa..b79e083bd4 100644 --- a/Content.Server/Body/Systems/InternalsSystem.cs +++ b/Content.Server/Body/Systems/InternalsSystem.cs @@ -23,6 +23,7 @@ public sealed class InternalsSystem : EntitySystem [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 _internalsQuery; @@ -38,15 +39,30 @@ public sealed class InternalsSystem : EntitySystem SubscribeLocalEvent>(OnGetInteractionVerbs); SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(OnStartingGear); + SubscribeLocalEvent(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( @@ -243,6 +259,7 @@ public sealed class InternalsSystem : EntitySystem public Entity? FindBestGasTank( Entity user) { + // TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses // Prioritise // 1. back equipped tanks // 2. exo-slot tanks diff --git a/Content.Server/Body/Systems/LungSystem.cs b/Content.Server/Body/Systems/LungSystem.cs index e83d3c32a2..7e58c24f7e 100644 --- a/Content.Server/Body/Systems/LungSystem.cs +++ b/Content.Server/Body/Systems/LungSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Atmos.EntitySystems; 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; @@ -77,23 +78,32 @@ public sealed class LungSystem : EntitySystem if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution)) return; - foreach (var gas in Enum.GetValues()) + GasToReagent(lung.Air, solution); + _solutionContainerSystem.UpdateChemicals(lung.Solution.Value); + } + + private void GasToReagent(GasMixture gas, Solution solution) + { + foreach (var gasId in Enum.GetValues()) { - 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; } } diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs index a46294beb4..4e6c02edbd 100644 --- a/Content.Server/Body/Systems/RespiratorSystem.cs +++ b/Content.Server/Body/Systems/RespiratorSystem.cs @@ -1,16 +1,21 @@ 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; @@ -26,9 +31,12 @@ public sealed class RespiratorSystem : EntitySystem [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 GasId = new("Gas"); + public override void Initialize() { base.Initialize(); @@ -109,7 +117,7 @@ public sealed class RespiratorSystem : EntitySystem // Inhale gas var ev = new InhaleLocationEvent(); - RaiseLocalEvent(uid, ref ev, broadcast: false); + RaiseLocalEvent(uid, ref ev); ev.Gas ??= _atmosSys.GetContainingMixture(uid, excite: true); @@ -164,6 +172,112 @@ public sealed class RespiratorSystem : EntitySystem _atmosSys.Merge(ev.Gas, outGas); } + /// + /// Check whether or not an entity can metabolize inhaled air without suffocating or taking damage (i.e., no toxic + /// gasses). + /// + public bool CanMetabolizeInhaledAir(Entity 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); + } + + /// + /// Check whether or not an entity can metabolize the given gas mixture without suffocating or taking damage + /// (i.e., no toxic gasses). + /// + public bool CanMetabolizeGas(Entity ent, GasMixture gas) + { + if (!Resolve(ent, ref ent.Comp)) + return false; + + var organs = _bodySystem.GetBodyOrganComponents(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; + } + + /// + /// Get the amount of saturation that would be generated if the lung were to metabolize the given solution. + /// + /// + /// 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. + /// + /// The reagents to metabolize + /// The entity doing the metabolizing + /// Whether or not any of the reagents would deal damage to the entity + private float GetSaturation(Solution solution, Entity 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(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 ent) { if (ent.Comp.SuffocationCycles == 2) diff --git a/Content.Server/Chemistry/ReagentEffectConditions/OrganType.cs b/Content.Server/Chemistry/ReagentEffectConditions/OrganType.cs index 4ae13b6a6e..986c3d79c8 100644 --- a/Content.Server/Chemistry/ReagentEffectConditions/OrganType.cs +++ b/Content.Server/Chemistry/ReagentEffectConditions/OrganType.cs @@ -25,9 +25,15 @@ namespace Content.Server.Chemistry.ReagentEffectConditions if (args.OrganEntity == null) return false; - if (args.EntityManager.TryGetComponent(args.OrganEntity.Value, out var metabolizer) - && metabolizer.MetabolizerTypes != null - && metabolizer.MetabolizerTypes.Contains(Type)) + return Condition(args.OrganEntity.Value, args.EntityManager); + } + + public bool Condition(Entity metabolizer, IEntityManager entMan) + { + metabolizer.Comp ??= entMan.GetComponentOrNull(metabolizer.Owner); + if (metabolizer.Comp != null + && metabolizer.Comp.MetabolizerTypes != null + && metabolizer.Comp.MetabolizerTypes.Contains(Type)) return ShouldHave; return !ShouldHave; } diff --git a/Content.Server/Damage/Systems/DamageOnLandSystem.cs b/Content.Server/Damage/Systems/DamageOnLandSystem.cs index 8f01e791ea..2e72e76e6d 100644 --- a/Content.Server/Damage/Systems/DamageOnLandSystem.cs +++ b/Content.Server/Damage/Systems/DamageOnLandSystem.cs @@ -22,7 +22,7 @@ namespace Content.Server.Damage.Systems private void OnAttemptPacifiedThrow(Entity 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"); } diff --git a/Content.Server/Electrocution/ElectrocutionSystem.cs b/Content.Server/Electrocution/ElectrocutionSystem.cs index 8291e97efe..67e60c9de4 100644 --- a/Content.Server/Electrocution/ElectrocutionSystem.cs +++ b/Content.Server/Electrocution/ElectrocutionSystem.cs @@ -166,7 +166,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem 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); @@ -183,7 +183,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem 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); diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs index a93157a175..bce3dc21c2 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs @@ -396,7 +396,7 @@ public sealed partial class ExplosionSystem // 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)); diff --git a/Content.Server/Projectiles/ProjectileSystem.cs b/Content.Server/Projectiles/ProjectileSystem.cs index f8c8ef64b7..970536f42b 100644 --- a/Content.Server/Projectiles/ProjectileSystem.cs +++ b/Content.Server/Projectiles/ProjectileSystem.cs @@ -51,7 +51,7 @@ public sealed class ProjectileSystem : SharedProjectileSystem if (modifiedDamage is not null && EntityManager.EntityExists(component.Shooter)) { - if (modifiedDamage.Any() && !deleted) + if (modifiedDamage.AnyPositive() && !deleted) { _color.RaiseEffect(Color.Red, new List { target }, Filter.Pvs(target, entityManager: EntityManager)); } diff --git a/Content.Server/Station/Systems/StationSpawningSystem.cs b/Content.Server/Station/Systems/StationSpawningSystem.cs index 05f5cc58bc..f175565a5a 100644 --- a/Content.Server/Station/Systems/StationSpawningSystem.cs +++ b/Content.Server/Station/Systems/StationSpawningSystem.cs @@ -204,7 +204,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem } var gearEquippedEv = new StartingGearEquippedEvent(entity.Value); - RaiseLocalEvent(entity.Value, ref gearEquippedEv, true); + RaiseLocalEvent(entity.Value, ref gearEquippedEv); if (profile != null) { diff --git a/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs b/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs index 838311c1aa..9da7606bcc 100644 --- a/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs +++ b/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs @@ -109,7 +109,7 @@ public sealed class BluespaceLockerSystem : EntitySystem // 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(); } @@ -326,7 +326,7 @@ public sealed class BluespaceLockerSystem : EntitySystem // Move contained air if (component.BehaviorProperties.TransportGas) { - target.Value.storageComponent.Air.CopyFromMutable(entityStorageComponent.Air); + target.Value.storageComponent.Air.CopyFrom(entityStorageComponent.Air); entityStorageComponent.Air.Clear(); } diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs index 986cac98dd..7247109e37 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -237,7 +237,7 @@ public sealed partial class GunSystem : SharedGunSystem { if (!Deleted(hitEntity)) { - if (dmg.Any()) + if (dmg.AnyPositive()) { _color.RaiseEffect(Color.Red, new List() { hitEntity }, Filter.Pvs(hitEntity, entityManager: EntityManager)); } diff --git a/Content.Shared/Atmos/GasMixture.cs b/Content.Shared/Atmos/GasMixture.cs index a676ed6720..0f1efba976 100644 --- a/Content.Shared/Atmos/GasMixture.cs +++ b/Content.Shared/Atmos/GasMixture.cs @@ -96,6 +96,11 @@ namespace Content.Shared.Atmos Volume = volume; } + public GasMixture(GasMixture toClone) + { + CopyFrom(toClone); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void MarkImmutable() { @@ -197,9 +202,12 @@ namespace Content.Shared.Atmos } [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; } diff --git a/Content.Shared/Damage/DamageSpecifier.cs b/Content.Shared/Damage/DamageSpecifier.cs index 8ab9116a97..7f505b807f 100644 --- a/Content.Shared/Damage/DamageSpecifier.cs +++ b/Content.Shared/Damage/DamageSpecifier.cs @@ -43,7 +43,7 @@ namespace Content.Shared.Damage /// /// /// Note that this being zero does not mean this damage has no effect. Healing in one type may cancel damage - /// in another. Consider using or instead. + /// in another. Consider using or instead. /// public FixedPoint2 GetTotal() { @@ -60,7 +60,7 @@ namespace Content.Shared.Damage /// Differs from as a damage specifier might contain entries with zeroes. /// This also returns false if the specifier only contains negative values. /// - public bool Any() + public bool AnyPositive() { foreach (var value in DamageDict.Values) { diff --git a/Content.Shared/Station/SharedStationSpawningSystem.cs b/Content.Shared/Station/SharedStationSpawningSystem.cs index acf00f7c6b..f352c9db63 100644 --- a/Content.Shared/Station/SharedStationSpawningSystem.cs +++ b/Content.Shared/Station/SharedStationSpawningSystem.cs @@ -142,7 +142,7 @@ public abstract class SharedStationSpawningSystem : EntitySystem if (raiseEvent) { var ev = new StartingGearEquippedEvent(entity); - RaiseLocalEvent(entity, ref ev, true); + RaiseLocalEvent(entity, ref ev); } } } diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs index 7fc440db47..2d9993096b 100644 --- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs +++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs @@ -499,7 +499,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem 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)) -- 2.51.2