Only auto-enable internals when necessary (#28248)

* Only auto-enable internals when necessary

* Add toxic gas check

* Rename Any -> AnyPositive
This commit is contained in:
Leon Friedrich
2024-05-31 14:28:11 +12:00
committed by GitHub
parent dee9634c01
commit 54337911d3
16 changed files with 189 additions and 34 deletions

View File

@@ -106,7 +106,7 @@ namespace Content.Server.Atmos.EntitySystems
if (tile?.Air == null) if (tile?.Air == null)
continue; continue;
tile.Air.CopyFromMutable(combined); tile.Air.CopyFrom(combined);
InvalidateVisuals(ent, tile); InvalidateVisuals(ent, tile);
} }

View File

@@ -23,6 +23,7 @@ public sealed class InternalsSystem : EntitySystem
[Dependency] private readonly GasTankSystem _gasTank = default!; [Dependency] private readonly GasTankSystem _gasTank = default!;
[Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly RespiratorSystem _respirator = default!;
private EntityQuery<InternalsComponent> _internalsQuery; private EntityQuery<InternalsComponent> _internalsQuery;
@@ -38,15 +39,30 @@ public sealed class InternalsSystem : EntitySystem
SubscribeLocalEvent<InternalsComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs); SubscribeLocalEvent<InternalsComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs);
SubscribeLocalEvent<InternalsComponent, InternalsDoAfterEvent>(OnDoAfter); 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; 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( private void OnGetInteractionVerbs(
@@ -243,6 +259,7 @@ public sealed class InternalsSystem : EntitySystem
public Entity<GasTankComponent>? FindBestGasTank( public Entity<GasTankComponent>? FindBestGasTank(
Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user) Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user)
{ {
// TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses
// Prioritise // Prioritise
// 1. back equipped tanks // 1. back equipped tanks
// 2. exo-slot tanks // 2. exo-slot tanks

View File

@@ -3,6 +3,7 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components; using Content.Server.Body.Components;
using Content.Server.Chemistry.Containers.EntitySystems; using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Chemistry.Components;
using Content.Shared.Clothing; using Content.Shared.Clothing;
using Content.Shared.Inventory.Events; 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)) if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution))
return; return;
foreach (var gas in Enum.GetValues<Gas>()) GasToReagent(lung.Air, solution);
_solutionContainerSystem.UpdateChemicals(lung.Solution.Value);
}
private void GasToReagent(GasMixture gas, Solution solution)
{ {
var i = (int) gas; foreach (var gasId in Enum.GetValues<Gas>())
var moles = lung.Air[i]; {
var i = (int) gasId;
var moles = gas[i];
if (moles <= 0) if (moles <= 0)
continue; continue;
var reagent = _atmosphereSystem.GasReagents[i]; var reagent = _atmosphereSystem.GasReagents[i];
if (reagent is null) continue; if (reagent is null)
continue;
var amount = moles * Atmospherics.BreathMolesToReagentMultiplier; var amount = moles * Atmospherics.BreathMolesToReagentMultiplier;
solution.AddReagent(reagent, amount); 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;
} }
} }

View File

@@ -1,16 +1,21 @@
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components; using Content.Server.Body.Components;
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Server.Chemistry.Containers.EntitySystems; using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Chemistry.ReagentEffectConditions;
using Content.Server.Chemistry.ReagentEffects;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Body.Components; 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.Damage;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Server.Body.Systems; namespace Content.Server.Body.Systems;
@@ -26,9 +31,12 @@ public sealed class RespiratorSystem : EntitySystem
[Dependency] private readonly DamageableSystem _damageableSys = default!; [Dependency] private readonly DamageableSystem _damageableSys = default!;
[Dependency] private readonly LungSystem _lungSystem = default!; [Dependency] private readonly LungSystem _lungSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly ChatSystem _chat = default!;
private static readonly ProtoId<MetabolismGroupPrototype> GasId = new("Gas");
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -109,7 +117,7 @@ public sealed class RespiratorSystem : EntitySystem
// Inhale gas // Inhale gas
var ev = new InhaleLocationEvent(); var ev = new InhaleLocationEvent();
RaiseLocalEvent(uid, ref ev, broadcast: false); RaiseLocalEvent(uid, ref ev);
ev.Gas ??= _atmosSys.GetContainingMixture(uid, excite: true); ev.Gas ??= _atmosSys.GetContainingMixture(uid, excite: true);
@@ -164,6 +172,112 @@ public sealed class RespiratorSystem : EntitySystem
_atmosSys.Merge(ev.Gas, outGas); _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) private void TakeSuffocationDamage(Entity<RespiratorComponent> ent)
{ {
if (ent.Comp.SuffocationCycles == 2) if (ent.Comp.SuffocationCycles == 2)

View File

@@ -25,9 +25,15 @@ namespace Content.Server.Chemistry.ReagentEffectConditions
if (args.OrganEntity == null) if (args.OrganEntity == null)
return false; return false;
if (args.EntityManager.TryGetComponent<MetabolizerComponent>(args.OrganEntity.Value, out var metabolizer) return Condition(args.OrganEntity.Value, args.EntityManager);
&& metabolizer.MetabolizerTypes != null }
&& metabolizer.MetabolizerTypes.Contains(Type))
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;
return !ShouldHave; return !ShouldHave;
} }

View File

@@ -22,7 +22,7 @@ namespace Content.Server.Damage.Systems
private void OnAttemptPacifiedThrow(Entity<DamageOnLandComponent> ent, ref AttemptPacifiedThrowEvent args) private void OnAttemptPacifiedThrow(Entity<DamageOnLandComponent> ent, ref AttemptPacifiedThrowEvent args)
{ {
// Allow healing projectiles, forbid any that do damage: // Allow healing projectiles, forbid any that do damage:
if (ent.Comp.Damage.Any()) if (ent.Comp.Damage.AnyPositive())
{ {
args.Cancel("pacified-cannot-throw"); args.Cancel("pacified-cannot-throw");
} }

View File

@@ -166,7 +166,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
if (!electrified.OnAttacked) if (!electrified.OnAttacked)
return; return;
if (!_meleeWeapon.GetDamage(args.Used, args.User).Any()) if (_meleeWeapon.GetDamage(args.Used, args.User).Empty)
return; return;
TryDoElectrifiedAct(uid, args.User, 1, electrified); TryDoElectrifiedAct(uid, args.User, 1, electrified);
@@ -183,7 +183,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
if (!component.CurrentLit || args.Used != args.User) if (!component.CurrentLit || args.Used != args.User)
return; return;
if (!_meleeWeapon.GetDamage(args.Used, args.User).Any()) if (_meleeWeapon.GetDamage(args.Used, args.User).Empty)
return; return;
DoCommonElectrocution(args.User, uid, component.UnarmedHitShock, component.UnarmedHitStun, false); DoCommonElectrocution(args.User, uid, component.UnarmedHitShock, component.UnarmedHitStun, false);

View File

@@ -396,7 +396,7 @@ public sealed partial class ExplosionSystem
// don't raise BeforeExplodeEvent if the entity is completely immune to explosions // don't raise BeforeExplodeEvent if the entity is completely immune to explosions
var thisDamage = GetDamage(uid, prototype, originalDamage); var thisDamage = GetDamage(uid, prototype, originalDamage);
if (!thisDamage.Any()) if (thisDamage.Empty)
return; return;
_toDamage.Add((uid, thisDamage)); _toDamage.Add((uid, thisDamage));

View File

@@ -51,7 +51,7 @@ public sealed class ProjectileSystem : SharedProjectileSystem
if (modifiedDamage is not null && EntityManager.EntityExists(component.Shooter)) 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)); _color.RaiseEffect(Color.Red, new List<EntityUid> { target }, Filter.Pvs(target, entityManager: EntityManager));
} }

View File

@@ -204,7 +204,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
} }
var gearEquippedEv = new StartingGearEquippedEvent(entity.Value); var gearEquippedEv = new StartingGearEquippedEvent(entity.Value);
RaiseLocalEvent(entity.Value, ref gearEquippedEv, true); RaiseLocalEvent(entity.Value, ref gearEquippedEv);
if (profile != null) if (profile != null)
{ {

View File

@@ -109,7 +109,7 @@ public sealed class BluespaceLockerSystem : EntitySystem
// Move contained air // Move contained air
if (component.BehaviorProperties.TransportGas) if (component.BehaviorProperties.TransportGas)
{ {
entityStorageComponent.Air.CopyFromMutable(target.Value.storageComponent.Air); entityStorageComponent.Air.CopyFrom(target.Value.storageComponent.Air);
target.Value.storageComponent.Air.Clear(); target.Value.storageComponent.Air.Clear();
} }
@@ -326,7 +326,7 @@ public sealed class BluespaceLockerSystem : EntitySystem
// Move contained air // Move contained air
if (component.BehaviorProperties.TransportGas) if (component.BehaviorProperties.TransportGas)
{ {
target.Value.storageComponent.Air.CopyFromMutable(entityStorageComponent.Air); target.Value.storageComponent.Air.CopyFrom(entityStorageComponent.Air);
entityStorageComponent.Air.Clear(); entityStorageComponent.Air.Clear();
} }

View File

@@ -237,7 +237,7 @@ public sealed partial class GunSystem : SharedGunSystem
{ {
if (!Deleted(hitEntity)) if (!Deleted(hitEntity))
{ {
if (dmg.Any()) if (dmg.AnyPositive())
{ {
_color.RaiseEffect(Color.Red, new List<EntityUid>() { hitEntity }, Filter.Pvs(hitEntity, entityManager: EntityManager)); _color.RaiseEffect(Color.Red, new List<EntityUid>() { hitEntity }, Filter.Pvs(hitEntity, entityManager: EntityManager));
} }

View File

@@ -96,6 +96,11 @@ namespace Content.Shared.Atmos
Volume = volume; Volume = volume;
} }
public GasMixture(GasMixture toClone)
{
CopyFrom(toClone);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkImmutable() public void MarkImmutable()
{ {
@@ -197,9 +202,12 @@ namespace Content.Shared.Atmos
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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); sample.Moles.CopyTo(Moles, 0);
Temperature = sample.Temperature; Temperature = sample.Temperature;
} }

View File

@@ -43,7 +43,7 @@ namespace Content.Shared.Damage
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Note that this being zero does not mean this damage has no effect. Healing in one type may cancel 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 <see cref="Any()"/> or <see cref="Empty"/> instead. /// in another. Consider using <see cref="AnyPositive"/> or <see cref="Empty"/> instead.
/// </remarks> /// </remarks>
public FixedPoint2 GetTotal() 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. /// 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. /// This also returns false if the specifier only contains negative values.
/// </summary> /// </summary>
public bool Any() public bool AnyPositive()
{ {
foreach (var value in DamageDict.Values) foreach (var value in DamageDict.Values)
{ {

View File

@@ -142,7 +142,7 @@ public abstract class SharedStationSpawningSystem : EntitySystem
if (raiseEvent) if (raiseEvent)
{ {
var ev = new StartingGearEquippedEvent(entity); var ev = new StartingGearEquippedEvent(entity);
RaiseLocalEvent(entity, ref ev, true); RaiseLocalEvent(entity, ref ev);
} }
} }
} }

View File

@@ -499,7 +499,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList); var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin:user); 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 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)) if (damageResult.DamageDict.TryGetValue("Blunt", out var bluntDamage))