]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Only auto-enable internals when necessary (#28248)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Fri, 31 May 2024 02:28:11 +0000 (14:28 +1200)
committerGitHub <noreply@github.com>
Fri, 31 May 2024 02:28:11 +0000 (12:28 +1000)
* Only auto-enable internals when necessary

* Add toxic gas check

* Rename Any -> AnyPositive

16 files changed:
Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs
Content.Server/Body/Systems/InternalsSystem.cs
Content.Server/Body/Systems/LungSystem.cs
Content.Server/Body/Systems/RespiratorSystem.cs
Content.Server/Chemistry/ReagentEffectConditions/OrganType.cs
Content.Server/Damage/Systems/DamageOnLandSystem.cs
Content.Server/Electrocution/ElectrocutionSystem.cs
Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs
Content.Server/Projectiles/ProjectileSystem.cs
Content.Server/Station/Systems/StationSpawningSystem.cs
Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs
Content.Server/Weapons/Ranged/Systems/GunSystem.cs
Content.Shared/Atmos/GasMixture.cs
Content.Shared/Damage/DamageSpecifier.cs
Content.Shared/Station/SharedStationSpawningSystem.cs
Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs

index c1b58f7a7728eb390f6fd6614ffd6ebeb72fc488..0d622f3067ad60ac96c69a3a87e5347d4a7be325 100644 (file)
@@ -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);
             }
 
index c1e1de2baad82749628a3a68d987ac73a578737d..b79e083bd467ce6e5a8cf94d28808143f5b3a32b 100644 (file)
@@ -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<InternalsComponent> _internalsQuery;
 
@@ -38,15 +39,30 @@ public sealed class InternalsSystem : EntitySystem
         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(
@@ -243,6 +259,7 @@ public sealed class InternalsSystem : EntitySystem
     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
index e83d3c32a25fb3c597615deeef096dc657336cd0..7e58c24f7e4a1163652cbf58e4c8da25ef6a2b0b 100644 (file)
@@ -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<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;
     }
 }
index a46294beb4d93a643ffc68bb004fa553bb11a3af..4e6c02edbd64d8d6e3db63d8f5eb377e50d43c93 100644 (file)
@@ -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<MetabolismGroupPrototype> 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);
     }
 
+    /// <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)
index 4ae13b6a6e484db4a90027131abfac791d647e62..986c3d79c8d2f2f99ae5071431d49ad6f08de97d 100644 (file)
@@ -25,9 +25,15 @@ namespace Content.Server.Chemistry.ReagentEffectConditions
             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;
         }
index 8f01e791ead4d1d206ef757a0d8699860564fec8..2e72e76e6d8361966647057d12c2a2d47ecee595 100644 (file)
@@ -22,7 +22,7 @@ namespace Content.Server.Damage.Systems
         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");
             }
index 8291e97efe44ae1edadfa45da29200d4c5fd7772..67e60c9de4660c0243fbfc09468d7e10ce3b4eee 100644 (file)
@@ -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);
index a93157a175372d7afe2e9827f5b28efc538eb69d..bce3dc21c233fada850ea5a8e9394018aa1cf24b 100644 (file)
@@ -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));
index f8c8ef64b79170436c4ab0a539d8a8cf49ad1634..970536f42b02728157f590d0bc23a826a2aa2922 100644 (file)
@@ -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<EntityUid> { target }, Filter.Pvs(target, entityManager: EntityManager));
             }
index 05f5cc58bcf523e1d613f18f15d50287e0dfe965..f175565a5a0e8b5124c260836063a4334f68d8ea 100644 (file)
@@ -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)
         {
index 838311c1aacc51565c6e6bff143d52e8fdc29d52..9da7606bcc58dea54bd7bf0fca2f7c8032ba3c1a 100644 (file)
@@ -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();
         }
 
index 986cac98ddf4b748e6bd021e7e4fbf677cd027b3..7247109e37fdaead775990be65b1f8103f3fe507 100644 (file)
@@ -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<EntityUid>() { hitEntity }, Filter.Pvs(hitEntity, entityManager: EntityManager));
                                 }
index a676ed672045fb41b09089dacd953e2b7ad0f779..0f1efba97666f60c55f56ea2006c179bc64f3e7d 100644 (file)
@@ -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;
         }
index 8ab9116a975e0f3fb78d394622200ea8f31ffed1..7f505b807f776820577470fd0945412e67626e34 100644 (file)
@@ -43,7 +43,7 @@ namespace Content.Shared.Damage
         /// </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()
         {
@@ -60,7 +60,7 @@ namespace Content.Shared.Damage
         /// 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)
             {
index acf00f7c6bf25aef32c35ebd1725ac45bb5d6c87..f352c9db63e932f44ef2819e2fef98aa3782ee1f 100644 (file)
@@ -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);
         }
     }
 }
index 7fc440db47940f50346c18b99a546a54416ddeb5..2d9993096bc5c42e5fbc0736bd59525fd7d8b5df 100644 (file)
@@ -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))