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)
continue;
tile.Air.CopyFromMutable(combined);
tile.Air.CopyFrom(combined);
InvalidateVisuals(ent, tile);
}

View 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

View 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)
{
var i = (int) gas;
var moles = lung.Air[i];
foreach (var gasId in Enum.GetValues<Gas>())
{
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;
}
}

View 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)

View 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;
}

View 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");
}

View 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);

View 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));

View 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));
}

View 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)
{

View 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();
}

View 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));
}

View 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;
}

View 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)
{

View 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);
}
}
}

View 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))